diff --git a/src/addons/calendar/calendar.module.ts b/src/addons/calendar/calendar.module.ts index 3f4020e31..408011879 100644 --- a/src/addons/calendar/calendar.module.ts +++ b/src/addons/calendar/calendar.module.ts @@ -70,7 +70,7 @@ const mainMenuChildrenRoutes: Routes = [ await AddonCalendar.initialize(); - AddonCalendar.scheduleAllSitesEventsNotifications(); + AddonCalendar.updateAllSitesEventReminders(); }, }, ], diff --git a/src/addons/calendar/pages/edit-event/edit-event.page.ts b/src/addons/calendar/pages/edit-event/edit-event.page.ts index b76cc4f96..78087afcc 100644 --- a/src/addons/calendar/pages/edit-event/edit-event.page.ts +++ b/src/addons/calendar/pages/edit-event/edit-event.page.ts @@ -31,9 +31,10 @@ import { AddonCalendarEventType, AddonCalendar, AddonCalendarSubmitCreateUpdateFormDataWSParams, + AddonCalendarReminderUnits, } from '../../services/calendar'; import { AddonCalendarOffline } from '../../services/calendar-offline'; -import { AddonCalendarEventReminder, AddonCalendarEventTypeOption, AddonCalendarHelper } from '../../services/calendar-helper'; +import { AddonCalendarEventTypeOption, AddonCalendarHelper } from '../../services/calendar-helper'; import { AddonCalendarSync, AddonCalendarSyncProvider } from '../../services/calendar-sync'; import { CoreSite } from '@classes/site'; import { Translate } from '@singletons'; @@ -156,7 +157,6 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { /** * Fetch the data needed to render the form. * - * @param refresh Whether it's refreshing data. * @return Promise resolved when done. */ protected async fetchData(): Promise { @@ -273,15 +273,14 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { return; } - const courseFillterFullname = (course: CoreCourseSearchedData | CoreEnrolledCourseData): Promise => - CoreFilterHelper.getFiltersAndFormatText(course.fullname, 'course', course.id) - .then((result) => { - course.fullname = result.text; - - return; - }).catch(() => { - // Ignore errors. - }); + const courseFillFullname = async (course: CoreCourseSearchedData | CoreEnrolledCourseData): Promise => { + try { + const result = await CoreFilterHelper.getFiltersAndFormatText(course.fullname, 'course', course.id); + course.fullname = result.text; + } catch { + // Ignore errors. + } + }; if (this.showAll) { // Remove site home from the list of courses. @@ -296,9 +295,9 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { // Format the name of the courses. if ('contacts' in courses[0]) { - await Promise.all((courses as CoreCourseSearchedData[]).map(courseFillterFullname)); + await Promise.all((courses as CoreCourseSearchedData[]).map(courseFillFullname)); } else { - await Promise.all((courses as CoreEnrolledCourseData[]).map(courseFillterFullname)); + await Promise.all((courses as CoreEnrolledCourseData[]).map(courseFillFullname)); } // Sort courses by name. @@ -461,17 +460,17 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { const timeDurationMinutes = parseInt(formData.timedurationminutes || '', 10); let error: string | undefined; - if (formData.eventtype == AddonCalendarEventType.COURSE && !formData.courseid) { + if (formData.eventtype === AddonCalendarEventType.COURSE && !formData.courseid) { error = 'core.selectacourse'; - } else if (formData.eventtype == AddonCalendarEventType.GROUP && !formData.groupcourseid) { + } else if (formData.eventtype === AddonCalendarEventType.GROUP && !formData.groupcourseid) { error = 'core.selectacourse'; - } else if (formData.eventtype == AddonCalendarEventType.GROUP && !formData.groupid) { + } else if (formData.eventtype === AddonCalendarEventType.GROUP && !formData.groupid) { error = 'core.selectagroup'; - } else if (formData.eventtype == AddonCalendarEventType.CATEGORY && !formData.categoryid) { + } else if (formData.eventtype === AddonCalendarEventType.CATEGORY && !formData.categoryid) { error = 'core.selectacategory'; - } else if (formData.duration == 1 && timeStartDate > timeUntilDate) { + } else if (formData.duration === 1 && timeStartDate > timeUntilDate) { error = 'addon.calendar.invalidtimedurationuntil'; - } else if (formData.duration == 2 && (isNaN(timeDurationMinutes) || timeDurationMinutes < 1)) { + } else if (formData.duration === 2 && (isNaN(timeDurationMinutes) || timeDurationMinutes < 1)) { error = 'addon.calendar.invalidtimedurationminutes'; } @@ -534,8 +533,11 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { if (result.sent) { // Event created or edited, invalidate right days & months. - const numberOfRepetitions = formData.repeat ? formData.repeats : - (data.repeateditall && this.otherEventsCount ? this.otherEventsCount + 1 : 1); + const numberOfRepetitions = formData.repeat + ? formData.repeats + : (data.repeateditall && this.otherEventsCount + ? this.otherEventsCount + 1 + : 1); try { await AddonCalendarHelper.refreshAfterChangeEvent(result.event, numberOfRepetitions); @@ -651,7 +653,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { // Check if default reminders are enabled. const defaultTime = await AddonCalendar.getDefaultNotificationTime(this.currentSite.getId()); - if (defaultTime === 0) { + if (defaultTime === AddonCalendarProvider.DEFAULT_NOTIFICATION_DISABLED) { return; } @@ -659,7 +661,6 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { // Add default reminder. this.reminders.push({ - time: null, value: data.value, unit: data.unit, label: AddonCalendar.getUnitValueLabel(data.value, data.unit, true), @@ -697,7 +698,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { */ removeReminder(reminder: AddonCalendarEventCandidateReminder): void { const index = this.reminders.indexOf(reminder); - if (index != -1) { + if (index !== -1) { this.reminders.splice(index, 1); } } @@ -712,4 +713,9 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { } -type AddonCalendarEventCandidateReminder = Omit; +type AddonCalendarEventCandidateReminder = { + time?: number; // Undefined for default reminder. + value: number; // Amount of time. + unit: AddonCalendarReminderUnits; // Units. + label: string; // Label to represent the reminder. +}; diff --git a/src/addons/calendar/pages/event/event.html b/src/addons/calendar/pages/event/event.html index 1a7587712..2fe34311c 100644 --- a/src/addons/calendar/pages/event/event.html +++ b/src/addons/calendar/pages/event/event.html @@ -139,7 +139,7 @@

{{ reminder.label }}

{{ reminder.sublabel }}

- diff --git a/src/addons/calendar/pages/event/event.page.ts b/src/addons/calendar/pages/event/event.page.ts index 101c8eb90..19fe931c1 100644 --- a/src/addons/calendar/pages/event/event.page.ts +++ b/src/addons/calendar/pages/event/event.page.ts @@ -42,6 +42,7 @@ import { AddonCalendarReminderTimeModalComponent } from '@addons/calendar/compon import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { AddonCalendarEventsSource } from '@addons/calendar/classes/events-source'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; +import { CoreReminders } from '@features/reminders/services/reminders'; /** * Page that displays a single calendar event. @@ -152,8 +153,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { return; } - const reminders = await AddonCalendar.getEventReminders(this.eventId, this.currentSiteId); - this.reminders = await AddonCalendarHelper.formatReminders(reminders, this.event.timestart, this.currentSiteId); + this.reminders = await AddonCalendarHelper.getEventReminders(this.eventId, this.event.timestart, this.currentSiteId); } /** @@ -402,12 +402,12 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { } /** - * Cancel the selected notification. + * Delete the selected reminder. * * @param id Reminder ID. * @param e Click event. */ - async cancelNotification(id: number, e: Event): Promise { + async deleteReminder(id: number, e: Event): Promise { e.preventDefault(); e.stopPropagation(); @@ -417,7 +417,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { const modal = await CoreDomUtils.showModalLoading('core.deleting', true); try { - await AddonCalendar.deleteEventReminder(id); + await CoreReminders.removeReminder(id); await this.loadReminders(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error deleting reminder'); diff --git a/src/addons/calendar/services/calendar-helper.ts b/src/addons/calendar/services/calendar-helper.ts index 09b58b831..8ab7902b9 100644 --- a/src/addons/calendar/services/calendar-helper.ts +++ b/src/addons/calendar/services/calendar-helper.ts @@ -23,7 +23,6 @@ import { AddonCalendarEventType, AddonCalendarGetEventsEvent, AddonCalendarProvider, - AddonCalendarReminderUnits, AddonCalendarWeek, AddonCalendarWeekDay, } from './calendar'; @@ -36,8 +35,8 @@ import { makeSingleton } from '@singletons'; import { AddonCalendarSyncInvalidateEvent } from './calendar-sync'; import { AddonCalendarOfflineEventDBRecord } from './database/calendar-offline'; import { CoreCategoryData } from '@features/courses/services/courses'; -import { AddonCalendarReminderDBRecord } from './database/calendar'; import { CoreTimeUtils } from '@services/utils/time'; +import { CoreReminders, CoreRemindersService } from '@features/reminders/services/reminders'; /** * Context levels enumeration. @@ -123,7 +122,7 @@ export class AddonCalendarHelperProvider { * Classify events into their respective months and days. If an event duration covers more than one day, * it will be included in all the days it lasts. * - * @param events Events to classify. + * @param offlineEvents Events to classify. * @return Object with the classified events. */ classifyIntoMonths( @@ -219,7 +218,7 @@ export class AddonCalendarHelperProvider { /** * Convenience function to format some event data to be rendered. * - * @param e Event to format. + * @param event Event to format. */ formatOfflineEventData(event: AddonCalendarOfflineEventDBRecord): AddonCalendarEventToDisplay { @@ -295,48 +294,75 @@ export class AddonCalendarHelperProvider { * @param timestart Event timestart. * @param siteId Site ID. * @return Formatted reminders. + * @deprecated since 4.1 Use AddonCalendarHelper.getEventReminders. */ async formatReminders( - reminders: AddonCalendarReminderDBRecord[], + reminders: { eventid: number }[], timestart: number, siteId?: string, ): Promise { - const defaultTime = await AddonCalendar.getDefaultNotificationTime(siteId); - - const formattedReminders = reminders; - const eventTimestart = timestart; - let defaultTimeValue: number | undefined; - let defaultTimeUnit: AddonCalendarReminderUnits | undefined; - - if (defaultTime > 0) { - const data = AddonCalendarProvider.convertSecondsToValueAndUnit(defaultTime); - defaultTimeValue = data.value; - defaultTimeUnit = data.unit; + if (!reminders.length) { + return []; } - return formattedReminders.map((reminder) => { - if (reminder.time === null) { + return AddonCalendarHelper.getEventReminders(reminders[0].eventid, timestart, siteId); + } + + /** + * Format reminders, adding calculated data. + * + * @param eventId Event Id. + * @param eventTimestart Event timestart. + * @param siteId Site ID. + * @return Formatted reminders. + */ + async getEventReminders( + eventId: number, + eventTimestart: number, + siteId?: string, + ): Promise { + const reminders = await CoreReminders.getReminders( + { + instanceId: eventId, + component: AddonCalendarProvider.COMPONENT, + }, + siteId, + ); + + if (!reminders.length) { + return []; + } + + const defaultTime = await AddonCalendar.getDefaultNotificationTime(siteId); + let defaultLabel: string | undefined; + + if (defaultTime > AddonCalendarProvider.DEFAULT_NOTIFICATION_DISABLED) { + const data = AddonCalendarProvider.convertSecondsToValueAndUnit(defaultTime); + defaultLabel = AddonCalendar.getUnitValueLabel(data.value, data.unit, true); + } + + return reminders.map((reminder) => { + const formatted: AddonCalendarEventReminder = { + id: reminder.id, + }; + + if (reminder.timebefore === CoreRemindersService.DEFAULT_REMINDER_TIMEBEFORE) { // Default time. Check if default notifications are disabled. - if (defaultTimeValue !== undefined && defaultTimeUnit) { - reminder.value = defaultTimeValue; - reminder.unit = defaultTimeUnit; - reminder.timestamp = eventTimestart - reminder.value * reminder.unit; + if (defaultLabel !== undefined) { + formatted.label = defaultLabel; + formatted.timestamp = eventTimestart - defaultTime; } } else { - const data = AddonCalendarProvider.convertSecondsToValueAndUnit(reminder.time); - reminder.value = data.value; - reminder.unit = data.unit; - reminder.timestamp = eventTimestart - reminder.time; + const data = AddonCalendarProvider.convertSecondsToValueAndUnit(reminder.timebefore); + formatted.label = AddonCalendar.getUnitValueLabel(data.value, data.unit, false); + formatted.timestamp = eventTimestart - reminder.timebefore; } - if (reminder.value && reminder.unit) { - reminder.label = AddonCalendar.getUnitValueLabel(reminder.value, reminder.unit, reminder.time === null); - if (reminder.timestamp) { - reminder.sublabel = CoreTimeUtils.userDate(reminder.timestamp * 1000, 'core.strftimedatetime'); - } + if (formatted.timestamp) { + formatted.sublabel = CoreTimeUtils.userDate(formatted.timestamp * 1000, 'core.strftimedatetime'); } - return reminder; + return formatted; }); } @@ -381,7 +407,7 @@ export class AddonCalendarHelperProvider { /** * Get the day "id". * - * @param day Day moment. + * @param moment Day moment. * @return The "id". */ getDayId(moment: moment.Moment): string { @@ -553,18 +579,18 @@ export class AddonCalendarHelperProvider { courseId: number, categoryId?: number, ): boolean { - if (event.eventtype == 'user' || event.eventtype == 'site') { + if (event.eventtype === 'user' || event.eventtype === 'site') { // User or site event, display it. return true; } - if (event.eventtype == 'category' && categories) { + if (event.eventtype === 'category' && categories) { if (!event.categoryid || !Object.keys(categories).length || !categoryId) { // We can't tell if the course belongs to the category, display them all. return true; } - if (event.categoryid == categoryId) { + if (event.categoryid === categoryId) { // The event is in the same category as the course, display it. return true; } @@ -577,7 +603,7 @@ export class AddonCalendarHelperProvider { break; } - if (event.categoryid == category.parent) { + if (event.categoryid === category.parent) { return true; } category = categories[category.parent]; @@ -796,9 +822,8 @@ export type AddonCalendarEventTypeOption = { /** * Formatted event reminder. */ -export type AddonCalendarEventReminder = AddonCalendarReminderDBRecord & { - value?: number; // Amount of time. - unit?: AddonCalendarReminderUnits; // Units. +export type AddonCalendarEventReminder = { + id: number; timestamp?: number; // Timestamp (in seconds). label?: string; // Label to represent the reminder. sublabel?: string; // Sub label. diff --git a/src/addons/calendar/services/calendar.ts b/src/addons/calendar/services/calendar.ts index f500eb1e6..ebe961573 100644 --- a/src/addons/calendar/services/calendar.ts +++ b/src/addons/calendar/services/calendar.ts @@ -23,12 +23,11 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreGroups } from '@services/groups'; import { CoreLocalNotifications } from '@services/local-notifications'; import { CoreConfig } from '@services/config'; -import { ILocalNotification } from '@ionic-native/local-notifications'; import { AddonCalendarOffline } from './calendar-offline'; import { CoreUser } from '@features/user/services/user'; import { CoreWSExternalWarning, CoreWSDate } from '@services/ws'; import moment from 'moment-timezone'; -import { AddonCalendarEventDBRecord, AddonCalendarReminderDBRecord, EVENTS_TABLE, REMINDERS_TABLE } from './database/calendar'; +import { AddonCalendarEventDBRecord, EVENTS_TABLE } from './database/calendar'; import { CoreCourses } from '@features/courses/services/courses'; import { ContextLevel, CoreConstants } from '@/core/constants'; import { CoreWSError } from '@classes/errors/wserror'; @@ -42,6 +41,13 @@ import { AddonCalendarSyncEvents, AddonCalendarSyncProvider } from './calendar-s import { CoreEvents } from '@singletons/events'; import { CoreText } from '@singletons/text'; import { CorePlatform } from '@services/platform'; +import { + CoreReminderData, + CoreReminders, + CoreRemindersPushNotificationData, + CoreRemindersService, +} from '@features/reminders/services/reminders'; +import { CoreReminderDBRecord } from '@features/reminders/services/database/reminders'; const ROOT_CACHE_KEY = 'mmaCalendar:'; @@ -121,6 +127,8 @@ export class AddonCalendarProvider { static readonly CALENDAR_TF_24 = '%H:%M'; // Calendar time in 24 hours format. static readonly CALENDAR_TF_12 = '%I:%M %p'; // Calendar time in 12 hours format. + static readonly DEFAULT_NOTIFICATION_DISABLED = 0; + protected weekDays: AddonCalendarWeekDaysTranslationKeys[] = [ { shortname: 'addon.calendar.sun', @@ -306,17 +314,12 @@ export class AddonCalendarProvider { EVENTS_TABLE, { id: eventId }, )); - promises.push(site.getDb().getRecords( - REMINDERS_TABLE, - { eventid: eventId }, - ).then((reminders) => - Promise.all(reminders.map((reminder) => this.deleteEventReminder(reminder.id, siteId))))); + promises.push(CoreReminders.removeReminders({ + instanceId: eventId, + component: AddonCalendarProvider.COMPONENT, + } , siteId)); - try { - await Promise.all(promises); - } catch { - // Ignore errors. - } + await CoreUtils.ignoreErrors(Promise.all(promises)); } /** @@ -326,55 +329,56 @@ export class AddonCalendarProvider { */ async initialize(): Promise { - CoreLocalNotifications.registerClick( + CoreLocalNotifications.registerClick( AddonCalendarProvider.COMPONENT, async (notification) => { - if (notification.eventId) { - await ApplicationInit.donePromise; + await ApplicationInit.donePromise; - const disabled = await this.isDisabled(notification.siteId); - if (disabled) { - // The calendar is disabled in the site, don't open it. - return; - } - - CoreNavigator.navigateToSitePath( - AddonCalendarMainMenuHandlerService.PAGE_NAME, - { - siteId: notification.siteId, - preferCurrentTab: false, - nextNavigation: { - path: `calendar/event/${notification.eventId}`, - isSitePath: true, - }, - }, - ); - } + this.notificationClicked(notification); }, ); - if (CoreLocalNotifications.isAvailable()) { - CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, async (data) => { - const site = await CoreSites.getSite(data.siteId); - - // Get all the events that have a default reminder. - const query = 'SELECT events.*, reminders.id AS reminderid ' + - 'FROM ' + EVENTS_TABLE + ' events ' + - 'INNER JOIN ' + REMINDERS_TABLE + ' reminders ON events.id = reminders.eventid ' + - 'WHERE reminders.time IS NULL'; - - const result = await site.getDb().execute(query); - - // Reschedule all the default reminders. - for (let i = 0; i < result.rows.length; i++) { - const event = result.rows.item(i) as AddonCalendarEventDBRecord & { - reminderid: number; - }; - - this.scheduleEventNotification(event, event.reminderid, null, site.getId()); - } - }); + if (!CoreLocalNotifications.isAvailable()) { + return; } + + CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, async (data) => { + const site = await CoreSites.getSite(data.siteId); + const siteId = site.getId(); + + // Get all the events that have a default reminder. + const reminders = await CoreReminders.getRemindersWithDefaultTime(AddonCalendarProvider.COMPONENT, siteId); + + // Reschedule all the default reminders. + reminders.forEach((reminder) => + CoreReminders.scheduleNotification(reminder, siteId)); + }); + } + + /** + * Notification has been clicked. + * + * @param notification Calendar notification. + * @return Promise resolved when done. + */ + async notificationClicked(notification: CoreRemindersPushNotificationData): Promise { + const disabled = await this.isDisabled(notification.siteId); + if (disabled) { + // The calendar is disabled in the site, don't open it. + return; + } + + CoreNavigator.navigateToSitePath( + AddonCalendarMainMenuHandlerService.PAGE_NAME, + { + siteId: notification.siteId, + preferCurrentTab: false, + nextNavigation: { + path: `calendar/event/${notification.instanceId}`, + isSitePath: true, + }, + }, + ); } /** @@ -527,7 +531,7 @@ export class AddonCalendarProvider { await site.read('core_calendar_get_allowed_event_types', params, preSets); // Convert the array to an object. - const result = {}; + const result: {[name: string]: boolean} = {}; if (response.allowedeventtypes) { response.allowedeventtypes.forEach((type) => { result[type] = true; @@ -699,7 +703,7 @@ export class AddonCalendarProvider { await site.read('core_calendar_get_calendar_event_by_id', params, preSets); this.storeEventInLocalDb(response.event, { siteId }); - this.scheduleEventsNotifications([response.event], siteId); + this.updateEventsReminders([response.event], site.getId()); return response.event; } catch (error) { @@ -766,27 +770,43 @@ export class AddonCalendarProvider { * Adds an event reminder and schedule a new notification. * * @param event Event to set the reminder. - * @param time Amount of seconds of the reminder. Undefined for default reminder. + * @param timebefore Amount of seconds of the reminder. Undefined for default reminder. * @param siteId ID of the site the event belongs to. If not defined, use current site. * @return Promise resolved when the notification is updated. */ async addEventReminder( - event: { id: number; timestart: number; name: string}, - time?: number | null, + event: AddonCalendarEvent | AddonCalendarEventDBRecord | AddonCalendarEventToDisplay | AddonCalendarOfflineEventDBRecord, + timebefore?: number, siteId?: string, ): Promise { - const site = await CoreSites.getSite(siteId); - const reminder: Partial = { - eventid: event.id, - time: time ?? null, - timecreated: Date.now(), + + timebefore = timebefore ?? CoreRemindersService.DEFAULT_REMINDER_TIMEBEFORE; + + const previousReminders = await CoreReminders.getReminders({ + instanceId: event.id, + component: AddonCalendarProvider.COMPONENT, + }, siteId); + + if (previousReminders.some((reminder) => reminder.timebefore === timebefore)) { + // Already exists. + return; + } + + const url = 'url' in event + ? event.url || '' + : ''; + + const reminder: CoreReminderData = { + component: AddonCalendarProvider.COMPONENT, + instanceId: event.id, + type: event.eventtype, + time: event.timestart, + timebefore, + title: event.name, + url, }; - const reminderId = await site.getDb().insertRecord(REMINDERS_TABLE, reminder); - - const timestamp = time ? event.timestart - time : time; - - await this.scheduleEventNotification(event, reminderId, timestamp, site.getId()); + await CoreReminders.addReminder(reminder, siteId); } /** @@ -810,15 +830,10 @@ export class AddonCalendarProvider { * @param id Reminder ID. * @param siteId ID of the site the event belongs to. If not defined, use current site. * @return Promise resolved when the notification is updated. + * @deprecated since 4.1. Use CoreReminders.removeReminder instead. */ async deleteEventReminder(id: number, siteId?: string): Promise { - const site = await CoreSites.getSite(siteId); - - if (CoreLocalNotifications.isAvailable()) { - CoreLocalNotifications.cancel(id, AddonCalendarProvider.COMPONENT, site.getId()); - } - - await site.getDb().deleteRecords(REMINDERS_TABLE, { id: id }); + await CoreReminders.removeReminder(id, siteId); } /** @@ -865,7 +880,7 @@ export class AddonCalendarProvider { } const response: AddonCalendarCalendarDay = await site.read('core_calendar_get_calendar_day_view', params, preSets); this.storeEventsInLocalDB(response.events, { siteId }); - this.scheduleEventsNotifications(response.events, siteId); + this.updateEventsReminders(response.events, site.getId()); return response; } @@ -909,14 +924,16 @@ export class AddonCalendarProvider { /** * Get a calendar reminders from local Db. * - * @param id Event ID. + * @param eventId Event ID. * @param siteId ID of the site the event belongs to. If not defined, use current site. * @return Promise resolved when the event data is retrieved. + * @deprecated since 4.1. Use CoreReminders.getReminders instead. */ - async getEventReminders(id: number, siteId?: string): Promise { - const site = await CoreSites.getSite(siteId); - - return site.getDb().getRecords(REMINDERS_TABLE, { eventid: id }, 'timecreated ASC, time ASC'); + async getEventReminders(eventId: number, siteId?: string): Promise { + return CoreReminders.getReminders({ + instanceId: eventId, + component: AddonCalendarProvider.COMPONENT, + }, siteId); } /** @@ -1070,7 +1087,7 @@ export class AddonCalendarProvider { response.weeks.forEach((week) => { week.days.forEach((day) => { this.storeEventsInLocalDB(day.events, { siteId }); - this.scheduleEventsNotifications(day.events, siteId); + this.updateEventsReminders(day.events, site.getId()); }); }); @@ -1184,7 +1201,7 @@ export class AddonCalendarProvider { const response = await site.read('core_calendar_get_calendar_upcoming_view', params, preSets); this.storeEventsInLocalDB(response.events, { siteId }); - this.scheduleEventsNotifications(response.events, siteId); + this.updateEventsReminders(response.events, site.getId()); return response; } @@ -1422,6 +1439,16 @@ export class AddonCalendarProvider { return this.isCalendarDisabledInSite(site); } + /** + * Get the next events for all the sites and schedules their notifications. + * + * @return Promise resolved when done. + * @deprecated since 4.1 Use AddonCalendar.updateAllSitesEventReminders. + */ + async scheduleAllSitesEventsNotifications(): Promise { + await AddonCalendar.updateAllSitesEventReminders(); + } + /** * Get the next events for all the sites and schedules their notifications. * If an event notification time is 0, cancel its scheduled notification (if any). @@ -1429,95 +1456,40 @@ export class AddonCalendarProvider { * * @return Promise resolved when all the notifications have been scheduled. */ - async scheduleAllSitesEventsNotifications(): Promise { + async updateAllSitesEventReminders(): Promise { await CorePlatform.ready(); - const notificationsEnabled = CoreLocalNotifications.isAvailable(); - - const siteIds = await CoreSites.getSitesIds(); - - const promises = siteIds.map((siteId: string) => async () => { - if (notificationsEnabled) { - // Check if calendar is disabled for the site. - const disabled = await this.isDisabled(siteId); - if (!disabled) { - // Get first events. - const events = await this.getEventsList(undefined, undefined, undefined, siteId); - await this.scheduleEventsNotifications(events, siteId); - } - } - - return; - }); - - await Promise.all(promises); - } - - /** - * Schedules an event notification. If time is 0, cancel scheduled notification if any. - * If local notification plugin is not enabled, resolve the promise. - * - * @param event Event to schedule. - * @param reminderId The reminder ID. - * @param time Notification timestamp (in seconds). Undefined for default time. - * @param siteId Site ID the event belongs to. If not defined, use current site. - * @return Promise resolved when the notification is scheduled. - */ - protected async scheduleEventNotification( - event: { id: number; timestart: number; name: string}, - reminderId: number, - time?: number | null, - siteId?: string, - ): Promise { - if (!CoreLocalNotifications.isAvailable()) { return; } + const siteIds = await CoreSites.getSitesIds(); + + await Promise.all(siteIds.map((siteId: string) => async () => { + + // Check if calendar is disabled for the site. + const disabled = await this.isDisabled(siteId); + if (!disabled) { + // Get first events. + const events = await this.getEventsList(undefined, undefined, undefined, siteId); + await this.updateEventsReminders(events, siteId); + } + })); + } + + /** + * Get the next events for all the sites and schedules their notifications. + * + * @return Promise resolved when done. + * @deprecated since 4.1. No replacement for that function. + */ + async scheduleEventsNotifications( + events: ({ id: number; timestart: number; timeduration: number; name: string})[], + siteId?: string, + ): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); - if (time === 0) { - // Cancel if it was scheduled. - return CoreLocalNotifications.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); - } - - if (!time) { - // Get event default time to calculate the notification time. - time = await this.getDefaultNotificationTime(siteId); - - if (time === 0) { - // Default notification time is disabled, do not show. - return CoreLocalNotifications.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); - } - - time = event.timestart - time; - } - - time = time * 1000; - - if (time <= Date.now()) { - // This reminder is over, don't schedule. Cancel if it was scheduled. - return CoreLocalNotifications.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); - } - - const notificationData: AddonCalendarPushNotificationData = { - eventId: event.id, - reminderId: reminderId, - siteId: siteId, - }; - - const notification: ILocalNotification = { - id: reminderId, - title: event.name, - text: CoreTimeUtils.userDate(event.timestart * 1000, 'core.strftimedaydatetime', true), - icon: 'file://assets/img/icons/calendar.png', - trigger: { - at: new Date(time), - }, - data: notificationData, - }; - - return CoreLocalNotifications.schedule(notification, AddonCalendarProvider.COMPONENT, siteId); + await AddonCalendar.updateEventsReminders(events, siteId); } /** @@ -1526,38 +1498,43 @@ export class AddonCalendarProvider { * If local notification plugin is not enabled, resolve the promise. * * @param events Events to schedule. - * @param siteId ID of the site the events belong to. If not defined, use current site. + * @param siteId ID of the site the events belong to. * @return Promise resolved when all the notifications have been scheduled. */ - async scheduleEventsNotifications( - events: ({ id: number; timestart: number; timeduration: number; name: string})[], - siteId?: string, + protected async updateEventsReminders( + events: ({ id: number; timestart: number; name: string})[], + siteId: string, ): Promise { if (!CoreLocalNotifications.isAvailable()) { return; } - siteId = siteId || CoreSites.getCurrentSiteId(); - - const promises = events.map(async (event) => { + await Promise.all(events.map(async (event) => { if (event.timestart * 1000 <= Date.now()) { // The event has already started, don't schedule it. - return; + + // @TODO Decide when to completelly remove expired events. + return CoreReminders.cancelReminder(event.id, AddonCalendarProvider.COMPONENT, siteId); } - const reminders = await this.getEventReminders(event.id, siteId); + const reminders = await CoreReminders.getReminders({ + instanceId: event.id, + component: AddonCalendarProvider.COMPONENT, + }, siteId); - const p2 = reminders.map((reminder) => { - const time = reminder.time ? event.timestart - reminder.time : reminder.time; + await Promise.all(reminders.map(async (reminder) => { + if (reminder.time !== event.timestart || reminder.title !== event.name) { + reminder.time = event.timestart; + reminder.title = event.name; - return this.scheduleEventNotification(event, reminder.id, time, siteId); - }); - - await Promise.all(p2); - }); - - await Promise.all(promises); + CoreReminders.updateReminder( + reminder, + siteId, + ); + } + })); + })); } /** @@ -1587,24 +1564,8 @@ export class AddonCalendarProvider { options: AddonCalendarStoreEventsOptions = {}, ): Promise { const site = await CoreSites.getSite(options.siteId); - const siteId = site.getId(); const addDefaultReminder = options.addDefaultReminder ?? true; - if (addDefaultReminder) { - // Add default reminder if the event isn't stored already and doesn't have any reminder. - try { - await this.getEventFromLocalDb(event.id, siteId); - } catch { - // Event does not exist. - const reminders = await this.getEventReminders(event.id, siteId); - - if (reminders.length === 0) { - // No reminders, create the default one. - this.addEventReminder(event, undefined, siteId); - } - } - } - // Don't store data that can be calculated like formattedtime, iscategoryevent, etc. let eventRecord: AddonCalendarEventDBRecord = { id: event.id, @@ -1659,9 +1620,40 @@ export class AddonCalendarProvider { }); } + if (addDefaultReminder) { + this.addDefaultEventReminder(eventRecord, site.getId()); + } + await site.getDb().insertRecord(EVENTS_TABLE, eventRecord); } + /** + * Adds the default event reminder. + * + * @param event Event to add the reminder to. + * @param siteId Site ID. If not defined, current site. + */ + protected async addDefaultEventReminder(event: AddonCalendarEventDBRecord, siteId?: string): Promise { + // Add default reminder if the event isn't stored already and doesn't have any reminder. + const eventExist = await CoreUtils.promiseWorks(this.getEventFromLocalDb(event.id, siteId)); + if (eventExist) { + return; + } + + const reminders = await CoreReminders.getReminders({ + instanceId: event.id, + component: AddonCalendarProvider.COMPONENT, + }, siteId); + + if (reminders.length > 0) { + // It already has reminders. + return; + } + + // No reminders, create the default one. + await this.addEventReminder(event, undefined, siteId); + } + /** * Store events in local DB. * @@ -1681,9 +1673,7 @@ export class AddonCalendarProvider { * * @param eventId ID of the event. Negative value to edit offline event. If undefined/null, create a new event. * @param formData Form data. - * @param timeCreated The time the event was created. Only if modifying a new offline event. - * @param forceOffline True to always save it in offline. - * @param siteId Site ID. If not defined, current site. + * @param options Calendar submit event options. * @return Promise resolved with the event and a boolean indicating if data was sent to server or stored in offline. */ async submitEvent( @@ -1701,7 +1691,8 @@ export class AddonCalendarProvider { // Now save the reminders if any. if (options.reminders?.length) { await CoreUtils.ignoreErrors( - Promise.all(options.reminders.map((reminder) => this.addEventReminder(event, reminder.time, siteId))), + Promise.all(options.reminders.map((reminder) => + this.addEventReminder(event, reminder.time, siteId))), ); } @@ -1723,7 +1714,8 @@ export class AddonCalendarProvider { // Now save the reminders if any. if (options.reminders?.length) { await CoreUtils.ignoreErrors( - Promise.all(options.reminders.map((reminder) => this.addEventReminder(event, reminder.time, siteId))), + Promise.all(options.reminders.map((reminder) => + this.addEventReminder(event, reminder.time, siteId))), ); } @@ -1753,6 +1745,7 @@ export class AddonCalendarProvider { siteId?: string, ): Promise { const site = await CoreSites.getSite(siteId); + siteId = site.getId(); // Add data that is "hidden" in web. formData.id = eventId > 0 ? eventId : 0; @@ -1780,9 +1773,16 @@ export class AddonCalendarProvider { } if (eventId < 0) { - // Offline event has been sent. Change reminders eventid if any. + // Offline event has been sent. Change reminders instanceId if any. await CoreUtils.ignoreErrors( - site.getDb().updateRecords(REMINDERS_TABLE, { eventid: result.event.id }, { eventid: eventId }), + CoreReminders.updateReminders( + { instanceId: result.event.id }, + { + instanceId: eventId, + component: AddonCalendarProvider.COMPONENT, + }, + siteId, + ), ); } @@ -2316,15 +2316,6 @@ export type AddonCalendarUpdatedEventEvent = { sent?: boolean; }; -/** - * Additional data sent in push notifications, with some calculated data. - */ -type AddonCalendarPushNotificationData = { - eventId: number; - reminderId: number; - siteId: string; -}; - /** * Value and unit for reminders. */ @@ -2338,7 +2329,7 @@ export type AddonCalendarValueAndUnit = { */ export type AddonCalendarSubmitEventOptions = { reminders?: { - time: number | null; + time?: number; }[]; forceOffline?: boolean; siteId?: string; // Site ID. If not defined, current site. diff --git a/src/addons/calendar/services/database/calendar.ts b/src/addons/calendar/services/database/calendar.ts index 16da851ac..43d626a07 100644 --- a/src/addons/calendar/services/database/calendar.ts +++ b/src/addons/calendar/services/database/calendar.ts @@ -22,10 +22,9 @@ import { AddonCalendar, AddonCalendarEventType, AddonCalendarProvider } from '.. * Database variables for AddonCalendarProvider service. */ export const EVENTS_TABLE = 'addon_calendar_events_3'; -export const REMINDERS_TABLE = 'addon_calendar_reminders_2'; export const CALENDAR_SITE_SCHEMA: CoreSiteSchema = { name: 'AddonCalendarProvider', - version: 4, + version: 5, canBeCleared: [EVENTS_TABLE], tables: [ { @@ -179,47 +178,8 @@ export const CALENDAR_SITE_SCHEMA: CoreSiteSchema = { }, ], }, - { - name: REMINDERS_TABLE, - columns: [ - { - name: 'id', - type: 'INTEGER', - primaryKey: true, - }, - { - name: 'eventid', - type: 'INTEGER', - }, - { - name: 'time', - type: 'INTEGER', - }, - { - name: 'timecreated', - type: 'INTEGER', - }, - ], - uniqueKeys: [ - ['eventid', 'time'], - ], - }, ], async migrate(db: SQLiteDB, oldVersion: number, siteId: string): Promise { - if (oldVersion < 3) { - // Migrate calendar events. New format @since 3.7. - let oldTable = 'addon_calendar_events_2'; - - try { - await db.tableExists(oldTable); - } catch { - // The v2 table doesn't exist, try with v1. - oldTable = 'addon_calendar_events'; - } - - await db.migrateTable(oldTable, EVENTS_TABLE); - } - if (oldVersion < 4) { // Migrate default notification time if it was changed. // Don't use getDefaultNotificationTime to be able to detect if the value was changed or not. @@ -230,50 +190,6 @@ export const CALENDAR_SITE_SCHEMA: CoreSiteSchema = { // Convert from minutes to seconds. AddonCalendar.setDefaultNotificationTime(defaultTime * 60, siteId); } - - // Migrate reminders. New format @since 4.0. - const oldTable = 'addon_calendar_reminders'; - - try { - await db.tableExists(oldTable); - } catch (error) { - // Old table does not exist, ignore. - return; - } - - const records = await db.getAllRecords(oldTable); - const events: Record = {}; - - await Promise.all(records.map(async (record) => { - // Get the event to compare the reminder time with the event time. - if (!events[record.eventid]) { - try { - events[record.eventid] = await db.getRecord(EVENTS_TABLE, { id: record.eventid }); - } catch { - // Event not found in local DB, shouldn't happen. Ignore the reminder. - return; - } - } - - if (!record.time || record.time === -1) { - // Default reminder. Use null now. - record.time = null; - } else if (record.time > events[record.eventid].timestart) { - // Reminder is after the event, ignore it. - return; - } else { - // Remove seconds from the old reminder, it could include seconds by mistake. - record.time = events[record.eventid].timestart - Math.floor(record.time / 60) * 60; - } - - return db.insertRecord(REMINDERS_TABLE, record); - })); - - try { - await db.dropTable(oldTable); - } catch (error) { - // Error deleting old table, ignore. - } } }, }; @@ -318,10 +234,3 @@ export type AddonCalendarEventDBRecord = { maxdaytimestamp?: number; draggable?: number; }; - -export type AddonCalendarReminderDBRecord = { - id: number; - eventid: number; - time: number | null; // Number of seconds before the event, null for default time. - timecreated?: number | null; -}; diff --git a/src/core/features/features.module.ts b/src/core/features/features.module.ts index e5c8ee34f..7b5d7e569 100644 --- a/src/core/features/features.module.ts +++ b/src/core/features/features.module.ts @@ -27,9 +27,11 @@ import { CoreGradesModule } from './grades/grades.module'; import { CoreH5PModule } from './h5p/h5p.module'; import { CoreLoginModule } from './login/login.module'; import { CoreMainMenuModule } from './mainmenu/mainmenu.module'; +import { CoreNativeModule } from '@features/native/native.module'; import { CorePushNotificationsModule } from './pushnotifications/pushnotifications.module'; import { CoreQuestionModule } from './question/question.module'; import { CoreRatingModule } from './rating/rating.module'; +import { CoreRemindersModule } from './reminders/reminders.module'; import { CoreSearchModule } from './search/search.module'; import { CoreSettingsModule } from './settings/settings.module'; import { CoreSharedFilesModule } from './sharedfiles/sharedfiles.module'; @@ -37,11 +39,10 @@ import { CoreSiteHomeModule } from './sitehome/sitehome.module'; import { CoreSitePluginsModule } from './siteplugins/siteplugins.module'; import { CoreStylesModule } from './styles/styles.module'; import { CoreTagModule } from './tag/tag.module'; -import { CoreUserToursModule } from './usertours/user-tours.module'; import { CoreUserModule } from './user/user.module'; +import { CoreUserToursModule } from './usertours/user-tours.module'; import { CoreViewerModule } from './viewer/viewer.module'; import { CoreXAPIModule } from './xapi/xapi.module'; -import { CoreNativeModule } from '@features/native/native.module'; @NgModule({ imports: [ @@ -61,15 +62,16 @@ import { CoreNativeModule } from '@features/native/native.module'; CorePushNotificationsModule, CoreQuestionModule, CoreRatingModule, + CoreRemindersModule, CoreSearchModule, CoreSettingsModule, CoreSharedFilesModule, CoreSiteHomeModule, CoreSitePluginsModule, - CoreTagModule, CoreStylesModule, - CoreUserToursModule, + CoreTagModule, CoreUserModule, + CoreUserToursModule, CoreViewerModule, CoreXAPIModule, diff --git a/src/core/features/reminders/reminders.module.ts b/src/core/features/reminders/reminders.module.ts new file mode 100644 index 000000000..9c010befc --- /dev/null +++ b/src/core/features/reminders/reminders.module.ts @@ -0,0 +1,42 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { APP_INITIALIZER, NgModule, Type } from '@angular/core'; +import { CORE_SITE_SCHEMAS } from '@services/sites'; +import { REMINDERS_SITE_SCHEMA } from './services/database/reminders'; +import { CoreReminders, CoreRemindersService } from './services/reminders'; + +export const CORE_REMINDERS_SERVICES: Type[] = [ + CoreRemindersService, +]; + +@NgModule({ + imports: [ + ], + providers: [ + { + provide: CORE_SITE_SCHEMAS, + useValue: [REMINDERS_SITE_SCHEMA], + multi: true, + }, + { + provide: APP_INITIALIZER, + multi: true, + useValue: async () => { + CoreReminders.scheduleAllNotifications(); + }, + }, + ], +}) +export class CoreRemindersModule {} diff --git a/src/core/features/reminders/services/database/reminders.ts b/src/core/features/reminders/services/database/reminders.ts new file mode 100644 index 000000000..3bb9d6abe --- /dev/null +++ b/src/core/features/reminders/services/database/reminders.ts @@ -0,0 +1,225 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { AddonCalendarProvider } from '@addons/calendar/services/calendar'; +import { AddonCalendarEventDBRecord, EVENTS_TABLE } from '@addons/calendar/services/database/calendar'; +import { SQLiteDB } from '@classes/sqlitedb'; +import { CoreSiteSchema } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreReminderData, CoreRemindersService } from '../reminders'; + +/** + * Database variables for CoreRemindersService service. + */ +export const REMINDERS_TABLE = 'core_reminders'; +export const REMINDERS_SITE_SCHEMA: CoreSiteSchema = { + name: 'CoreRemindersService', + version: 1, + canBeCleared: [], + tables: [ + { + name: REMINDERS_TABLE, + columns: [ + { + name: 'id', + type: 'INTEGER', + primaryKey: true, + }, + { + name: 'component', + type: 'TEXT', + notNull: true, + }, + { + name: 'instanceId', + type: 'INTEGER', + notNull: true, + }, + { + name: 'type', + type: 'TEXT', + notNull: true, + }, + { + name: 'time', + type: 'INTEGER', + notNull: true, + }, + { + name: 'timebefore', + type: 'INTEGER', + notNull: true, + }, + { + name: 'title', + type: 'TEXT', + notNull: true, + }, + { + name: 'url', + type: 'TEXT', + }, + + ], + uniqueKeys: [ + ['component', 'instanceId', 'timebefore'], + ], + }, + ], + install: async (db: SQLiteDB): Promise => { + await migrateFromCalendarRemindersV1(db); + await migrateFromCalendarRemindersV2(db); + }, +}; + +const migrateFromCalendarRemindersV1 = async (db: SQLiteDB): Promise => { + // Migrate reminders. New format @since 4.0. + const oldTable = 'addon_calendar_reminders'; + + const tableExists = await CoreUtils.promiseWorks(db.tableExists(oldTable)); + if (!tableExists) { + return; + } + + const records = await db.getAllRecords(oldTable); + const events: Record = {}; + const uniqueReminder: Record = {}; + + await Promise.all(records.map(async (record) => { + // Get the event to compare the reminder time with the event time. + if (!events[record.eventid]) { + try { + events[record.eventid] = await db.getRecord(EVENTS_TABLE, { id: record.eventid }); + } catch { + // Event not found in local DB, shouldn't happen. Ignore the reminder. + return; + } + } + + const event = events[record.eventid]; + + let reminderTime = record.time; + + if (!reminderTime || reminderTime === -1) { + // Default reminder. + reminderTime = CoreRemindersService.DEFAULT_REMINDER_TIMEBEFORE; + } else if (reminderTime > event.timestart) { + // Reminder is after the event, ignore it. + return; + } else { + // Remove seconds from the old reminder, it could include seconds by mistake. + reminderTime = event.timestart - Math.floor(reminderTime / 60) * 60; + } + + if (typeof uniqueReminder[record.eventid] === undefined) { + uniqueReminder[record.eventid] = []; + } else { + if (uniqueReminder[record.eventid].includes(reminderTime)) { + // Reminder already exists. + return; + } + } + + await createReminder(db, event, reminderTime); + })); + + try { + await db.dropTable(oldTable); + } catch { + // Error deleting old table, ignore. + } +}; + +const migrateFromCalendarRemindersV2 = async (db: SQLiteDB): Promise => { + const oldTable = 'addon_calendar_reminders_2'; + + const tableExists = await CoreUtils.promiseWorks(db.tableExists(oldTable)); + if (!tableExists) { + return; + } + + const records = await db.getAllRecords(oldTable); + const events: Record = {}; + const uniqueReminder: Record = {}; + + await Promise.all(records.map(async (record) => { + // Get the event to compare the reminder time with the event time. + if (!events[record.eventid]) { + try { + events[record.eventid] = await db.getRecord(EVENTS_TABLE, { id: record.eventid }); + } catch { + // Event not found in local DB, shouldn't happen. Ignore the reminder. + return; + } + } + const event = events[record.eventid]; + + const reminderTime = record.time || CoreRemindersService.DEFAULT_REMINDER_TIMEBEFORE; + + if (typeof uniqueReminder[record.eventid] === undefined) { + uniqueReminder[record.eventid] = []; + } else { + if (uniqueReminder[record.eventid].includes(reminderTime)) { + // Reminder already exists. + return; + } + } + + uniqueReminder[record.eventid].push(reminderTime); + + await createReminder(db, event, reminderTime); + })); + + try { + await db.dropTable(oldTable); + } catch { + // Error deleting old table, ignore. + } +}; + +const createReminder = async ( + db: SQLiteDB, + event: AddonCalendarEventDBRecord, + reminderTime: number, +): Promise => { + const reminder: CoreReminderData = { + component: AddonCalendarProvider.COMPONENT, + instanceId: event.id, + type: event.eventtype, + timebefore: reminderTime, + url: event.url, + title: event.name, + time: event.timestart, + }; + + await db.insertRecord(REMINDERS_TABLE, reminder); +}; + +export type CoreReminderDBRecord = { + id: number; // Reminder ID. + component: string; // Component where the reminder belongs. + instanceId: number; // Instance Id where the reminder belongs. + type: string; // Event idenfier type. + time: number; // Event time. + timebefore: number; // Seconds before the event to remind. + title: string; // Notification title. + url?: string; // URL where to redirect the user. +}; + +type AddonCalendarReminderDBRecord = { + id: number; + eventid: number; + time: number | null; // Number of seconds before the event, null for default time. + timecreated?: number | null; +}; diff --git a/src/core/features/reminders/services/reminders.ts b/src/core/features/reminders/services/reminders.ts new file mode 100644 index 000000000..a71cf1063 --- /dev/null +++ b/src/core/features/reminders/services/reminders.ts @@ -0,0 +1,300 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { AddonCalendar, AddonCalendarProvider } from '@addons/calendar/services/calendar'; +import { Injectable } from '@angular/core'; +import { CoreLocalNotifications } from '@services/local-notifications'; +import { CoreSites } from '@services/sites'; +import { CoreTimeUtils } from '@services/utils/time'; +import { makeSingleton } from '@singletons'; +import { CoreReminderDBRecord, REMINDERS_TABLE } from './database/reminders'; +import { ILocalNotification } from '@ionic-native/local-notifications'; +import { CorePlatform } from '@services/platform'; + +/** + * Service to handle reminders. + */ +@Injectable({ providedIn: 'root' }) +export class CoreRemindersService { + + static readonly DEFAULT_REMINDER_TIMEBEFORE = -1; + + /** + * Returns if Reminders are enabled. + * + * @return True if reminders are enabled and available, false otherwise. + */ + isEnabled(): boolean { + return CoreLocalNotifications.isAvailable(); + } + + /** + * Save reminder to Database. + * + * @param reminder Reminder to set. + * @param siteId Site ID. If not defined, current site. + * @return Resolved when done. Rejected on failure. + */ + async addReminder(reminder: CoreReminderData, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const reminderId = await site.getDb().insertRecord(REMINDERS_TABLE, reminder); + + const reminderRecord: CoreReminderDBRecord = Object.assign(reminder, { id: reminderId }); + + await this.scheduleNotification(reminderRecord, site.getId()); + } + + /** + * Update a reminder from local Db. + * + * @param reminder Fields to update. + * @param siteId ID of the site the reminder belongs to. If not defined, use current site. + * @return Promise resolved when the reminder data is updated. + */ + async updateReminder( + reminder: CoreReminderDBRecord, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + await site.getDb().updateRecords(REMINDERS_TABLE, reminder, { id: reminder.id }); + + // Reschedule. + await this.scheduleNotification(reminder, siteId); + } + + /** + * Update all reminders of a component and instance from local Db. + * + * @param newFields Fields to update. + * @param selector Reminder selector. + * @param siteId ID of the site the reminder belongs to. If not defined, use current site. + * @return Promise resolved when the reminder data is updated. + */ + async updateReminders( + newFields: Partial, + selector: CoreReminderSelector, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const reminders = await this.getReminders(selector, site.getId()); + + await Promise.all(reminders.map((reminder) => { + reminder = Object.assign(reminder, newFields); + + return this.updateReminder(reminder, site.getId()); + })); + } + + /** + * Get all reminders from local Db. + * + * @param siteId ID of the site the reminder belongs to. If not defined, use current site. + * @return Promise resolved when the reminder data is retrieved. + */ + async getAllReminders( siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.getDb().getRecords(REMINDERS_TABLE, undefined, 'time ASC'); + } + + /** + * Get all reminders of a component and instance from local Db. + * + * @param selector Reminder selector. + * @param siteId ID of the site the reminder belongs to. If not defined, use current site. + * @return Promise resolved when the reminder data is retrieved. + */ + async getReminders(selector: CoreReminderSelector, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.getDb().getRecords(REMINDERS_TABLE, selector, 'time ASC'); + } + + /** + * Get all reminders of a component with default time. + * + * @param component Component. + * @param siteId ID of the site the reminder belongs to. If not defined, use current site. + * @return Promise resolved when the reminder data is retrieved. + */ + async getRemindersWithDefaultTime(component: string, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.getDb().getRecords( + REMINDERS_TABLE, + { component, timebefore: CoreRemindersService.DEFAULT_REMINDER_TIMEBEFORE }, + 'time ASC', + ); + } + + /** + * Remove a reminder and cancel the notification. + * + * @param id Reminder ID. + * @param siteId ID of the site the reminder belongs to. If not defined, use current site. + * @return Promise resolved when the notification is updated. + */ + async removeReminder(id: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const reminder = await site.getDb().getRecord(REMINDERS_TABLE, { id }); + + if (this.isEnabled()) { + this.cancelReminder(id, reminder.component, site.getId()); + } + + await site.getDb().deleteRecords(REMINDERS_TABLE, { id }); + } + + /** + * Remove all reminders of the same element. + * + * @param selector Reminder selector. + * @param siteId ID of the site the reminder belongs to. If not defined, use current site. + * @return Promise resolved when the notification is updated. + */ + async removeReminders(selector: CoreReminderSelector, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + siteId = site.getId(); + + if (this.isEnabled()) { + const reminders = await this.getReminders(selector, siteId); + + reminders.forEach((reminder) => { + this.cancelReminder(reminder.id, reminder.component, siteId); + }); + } + + await site.getDb().deleteRecords(REMINDERS_TABLE, selector); + } + + /** + * Cancel a notification for a reminder. + * + * @param reminderId Reminder Id to cancel. + * @param component Reminder component. + * @param siteId ID of the site the reminder belongs to. If not defined, use current site. + * @returns + */ + async cancelReminder(reminderId: number, component: string, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + return CoreLocalNotifications.cancel(reminderId, component, siteId); + } + + /** + * Schedules a notification. If local notification plugin is not enabled, resolve the promise. + * + * @param reminder Reminder to schedule. + * @param siteId Site ID the reminder belongs to. If not defined, use current site. + * @return Promise resolved when the notification is scheduled. + */ + async scheduleNotification( + reminder: CoreReminderDBRecord, + siteId?: string, + ): Promise { + + if (!this.isEnabled()) { + return; + } + + siteId = siteId || CoreSites.getCurrentSiteId(); + + const timebefore = reminder.timebefore === CoreRemindersService.DEFAULT_REMINDER_TIMEBEFORE + ? await AddonCalendar.getDefaultNotificationTime(siteId) + : reminder.timebefore; + + if (timebefore === AddonCalendarProvider.DEFAULT_NOTIFICATION_DISABLED) { + // Notification disabled. Cancel. + return this.cancelReminder(reminder.id, reminder.component, siteId); + } + + const notificationTime = (reminder.time - timebefore) * 1000; + + if (notificationTime <= Date.now()) { // @TODO Add a threshold. + // This reminder is over, don't schedule. Cancel if it was scheduled. + return this.cancelReminder(reminder.id, reminder.component, siteId); + } + + const notificationData: CoreRemindersPushNotificationData = { + reminderId: reminder.id, + instanceId: reminder.instanceId, + siteId: siteId, + }; + + const notification: ILocalNotification = { + id: reminder.id, + title: reminder.title, + text: CoreTimeUtils.userDate(reminder.time * 1000, 'core.strftimedaydatetime', true), + icon: 'file://assets/img/icons/calendar.png', + trigger: { + at: new Date(notificationTime), + }, + data: notificationData, + }; + + return CoreLocalNotifications.schedule(notification, reminder.component, siteId); + } + + /** + * Get the all saved reminders and schedule the notification. + * If local notification plugin is not enabled, resolve the promise. + * + * @return Promise resolved when all the notifications have been scheduled. + */ + async scheduleAllNotifications(): Promise { + await CorePlatform.ready(); + + if (!this.isEnabled()) { + return; + } + + const siteIds = await CoreSites.getSitesIds(); + + await Promise.all(siteIds.map((siteId: string) => async () => { + const reminders = await this.getAllReminders(siteId); + + reminders.forEach((reminder) => { + this.scheduleNotification(reminder, siteId); + }); + })); + } + +} + +export const CoreReminders = makeSingleton(CoreRemindersService); + +export type CoreReminderData = Omit; + +/** + * Additional data sent in push notifications, with some calculated data. + */ +export type CoreRemindersPushNotificationData = { + reminderId: number; + instanceId: number; + siteId: string; +}; + +export type CoreReminderNotificationOptions = { + title: string; +}; + +export type CoreReminderSelector = { + instanceId: number; + component: string; + type?: string; +}; diff --git a/src/core/services/local-notifications.ts b/src/core/services/local-notifications.ts index 17bb9a503..bfc61eabb 100644 --- a/src/core/services/local-notifications.ts +++ b/src/core/services/local-notifications.ts @@ -123,7 +123,7 @@ export class CoreLocalNotificationsProvider { async initializeDatabase(): Promise { try { await CoreApp.createTablesFromSchema(APP_SCHEMA); - } catch (e) { + } catch { // Ignore errors. }