From 47c913f434daa09fd4071cc3fcfe601ec989276c Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 10 Nov 2021 13:30:31 +0100 Subject: [PATCH] MOBILE-3909 calendar: Change reminders to time before --- scripts/langindex.json | 7 + .../calendar/components/components.module.ts | 3 + .../reminder-time-modal.html | 59 +++++++ .../reminder-time-modal.ts | 156 ++++++++++++++++++ src/addons/calendar/lang.json | 4 +- src/addons/calendar/pages/event/event.html | 20 +-- src/addons/calendar/pages/event/event.page.ts | 111 +++++++------ .../calendar/pages/settings/settings.html | 14 +- .../calendar/pages/settings/settings.ts | 51 +++++- .../calendar/services/calendar-helper.ts | 61 +++++++ src/addons/calendar/services/calendar.ts | 150 ++++++++++++++--- .../calendar/services/database/calendar.ts | 52 +++++- src/core/lang.json | 5 + 13 files changed, 581 insertions(+), 112 deletions(-) create mode 100644 src/addons/calendar/components/reminder-time-modal/reminder-time-modal.html create mode 100644 src/addons/calendar/components/reminder-time-modal/reminder-time-modal.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 489993f04..0f108b2c5 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -140,6 +140,7 @@ "addon.calendar.sunday": "calendar", "addon.calendar.thu": "calendar", "addon.calendar.thursday": "calendar", + "addon.calendar.timebefore": "local_moodlemobileapp", "addon.calendar.today": "calendar", "addon.calendar.tomorrow": "calendar", "addon.calendar.tue": "calendar", @@ -153,6 +154,7 @@ "addon.calendar.typeopen": "calendar", "addon.calendar.typesite": "calendar", "addon.calendar.typeuser": "calendar", + "addon.calendar.units": "qtype_numerical", "addon.calendar.upcomingevents": "calendar", "addon.calendar.userevents": "calendar", "addon.calendar.wed": "calendar", @@ -1562,6 +1564,7 @@ "core.courses.therearecourses": "moodle", "core.courses.totalcoursesearchresults": "local_moodlemobileapp", "core.currentdevice": "local_moodlemobileapp", + "core.custom": "form", "core.datastoredoffline": "local_moodlemobileapp", "core.date": "moodle", "core.day": "moodle", @@ -1943,6 +1946,8 @@ "core.maxsizeandattachments": "moodle", "core.min": "moodle", "core.mins": "moodle", + "core.minute": "moodle", + "core.minutes": "moodle", "core.misc": "admin", "core.mod_assign": "assign/pluginname", "core.mod_assignment": "assignment/pluginname", @@ -2283,6 +2288,8 @@ "core.viewprofile": "moodle", "core.warningofflinedatadeleted": "local_moodlemobileapp", "core.warnopeninbrowser": "local_moodlemobileapp", + "core.week": "moodle", + "core.weeks": "moodle", "core.whatisyourage": "moodle", "core.wheredoyoulive": "moodle", "core.whoissiteadmin": "local_moodlemobileapp", diff --git a/src/addons/calendar/components/components.module.ts b/src/addons/calendar/components/components.module.ts index 22b2b7100..283909027 100644 --- a/src/addons/calendar/components/components.module.ts +++ b/src/addons/calendar/components/components.module.ts @@ -19,12 +19,14 @@ import { CoreSharedModule } from '@/core/shared.module'; import { AddonCalendarCalendarComponent } from './calendar/calendar'; import { AddonCalendarUpcomingEventsComponent } from './upcoming-events/upcoming-events'; import { AddonCalendarFilterPopoverComponent } from './filter/filter'; +import { AddonCalendarReminderTimeModalComponent } from './reminder-time-modal/reminder-time-modal'; @NgModule({ declarations: [ AddonCalendarCalendarComponent, AddonCalendarUpcomingEventsComponent, AddonCalendarFilterPopoverComponent, + AddonCalendarReminderTimeModalComponent, ], imports: [ CoreSharedModule, @@ -35,6 +37,7 @@ import { AddonCalendarFilterPopoverComponent } from './filter/filter'; AddonCalendarCalendarComponent, AddonCalendarUpcomingEventsComponent, AddonCalendarFilterPopoverComponent, + AddonCalendarReminderTimeModalComponent, ], }) export class AddonCalendarComponentsModule {} diff --git a/src/addons/calendar/components/reminder-time-modal/reminder-time-modal.html b/src/addons/calendar/components/reminder-time-modal/reminder-time-modal.html new file mode 100644 index 000000000..ad47dee3a --- /dev/null +++ b/src/addons/calendar/components/reminder-time-modal/reminder-time-modal.html @@ -0,0 +1,59 @@ + + +

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

+ + + + + +
+
+ + + + + +

{{ 'core.settings.disabled' | translate }}

+
+ +
+ + +

{{ option.label }}

+
+ +
+ + + + +

{{ 'core.custom' | translate }}

+
+ +
+ + + +
+ + + + + + + + + {{ option.label | translate }} + + +
+
+
+ + + {{ 'core.done' | translate }} + +
diff --git a/src/addons/calendar/components/reminder-time-modal/reminder-time-modal.ts b/src/addons/calendar/components/reminder-time-modal/reminder-time-modal.ts new file mode 100644 index 000000000..f505fde21 --- /dev/null +++ b/src/addons/calendar/components/reminder-time-modal/reminder-time-modal.ts @@ -0,0 +1,156 @@ +// (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, AddonCalendarReminderUnits, AddonCalendarValueAndUnit } from '@addons/calendar/services/calendar'; +import { Component, Input, OnInit } from '@angular/core'; +import { CoreDomUtils } from '@services/utils/dom'; +import { ModalController } from '@singletons'; + +/** + * Modal to choose a reminder time. + */ +@Component({ + selector: 'addon-calendar-new-reminder-modal', + templateUrl: 'reminder-time-modal.html', +}) +export class AddonCalendarReminderTimeModalComponent implements OnInit { + + @Input() initialValue?: AddonCalendarValueAndUnit; + @Input() allowDisable?: boolean; + + radioValue = '5m'; + customValue = '10'; + customUnits = AddonCalendarReminderUnits.MINUTE; + + presetOptions = [ + { + radioValue: '5m', + value: 5, + unit: AddonCalendarReminderUnits.MINUTE, + label: '', + }, + { + radioValue: '10m', + value: 10, + unit: AddonCalendarReminderUnits.MINUTE, + label: '', + }, + { + radioValue: '30m', + value: 30, + unit: AddonCalendarReminderUnits.MINUTE, + label: '', + }, + { + radioValue: '1h', + value: 1, + unit: AddonCalendarReminderUnits.HOUR, + label: '', + }, + { + radioValue: '12h', + value: 12, + unit: AddonCalendarReminderUnits.HOUR, + label: '', + }, + { + radioValue: '1d', + value: 1, + unit: AddonCalendarReminderUnits.DAY, + label: '', + }, + ]; + + customUnitsOptions = [ + { + value: AddonCalendarReminderUnits.MINUTE, + label: 'core.minutes', + }, + { + value: AddonCalendarReminderUnits.HOUR, + label: 'core.hours', + }, + { + value: AddonCalendarReminderUnits.DAY, + label: 'core.days', + }, + { + value: AddonCalendarReminderUnits.WEEK, + label: 'core.weeks', + }, + ]; + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + this.presetOptions.forEach((option) => { + option.label = AddonCalendar.getUnitValueLabel(option.value, option.unit); + }); + + if (!this.initialValue) { + return; + } + + if (this.initialValue.value === 0) { + this.radioValue = 'disabled'; + } else { + // Search if it's one of the preset options. + const option = this.presetOptions.find(option => + option.value === this.initialValue?.value && option.unit === this.initialValue.unit); + + if (option) { + this.radioValue = option.radioValue; + } else { + // It's a custom value. + this.radioValue = 'custom'; + this.customValue = String(this.initialValue.value); + this.customUnits = this.initialValue.unit; + } + } + } + + /** + * Close the modal. + */ + closeModal(): void { + ModalController.dismiss(); + } + + /** + * Save the reminder. + */ + saveReminder(): void { + if (this.radioValue === 'disabled') { + ModalController.dismiss(0); + } else if (this.radioValue === 'custom') { + const value = parseInt(this.customValue, 10); + if (!value) { + CoreDomUtils.showErrorModal('core.errorinvalidform', true); + + return; + } + + ModalController.dismiss(Math.abs(value) * this.customUnits); + } else { + const option = this.presetOptions.find(option => option.radioValue === this.radioValue); + if (!option) { + return; + } + + ModalController.dismiss(option.unit * option.value); + } + } + +} diff --git a/src/addons/calendar/lang.json b/src/addons/calendar/lang.json index 8b1748f6b..a71e8921b 100644 --- a/src/addons/calendar/lang.json +++ b/src/addons/calendar/lang.json @@ -41,6 +41,7 @@ "noevents": "There are no events", "nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event.", "reminders": "Reminders", + "units": "Units", "repeatedevents": "Repeated events", "repeateditall": "Also apply changes to the other {{$a}} events in this repeat series", "repeateditthis": "Apply changes to this event only", @@ -54,6 +55,7 @@ "sunday": "Sunday", "thu": "Thu", "thursday": "Thursday", + "timebefore": "{{value}} {{units}} before", "today": "Today", "tomorrow": "Tomorrow", "tue": "Tue", @@ -73,4 +75,4 @@ "wednesday": "Wednesday", "when": "When", "yesterday": "Yesterday" -} \ No newline at end of file +} diff --git a/src/addons/calendar/pages/event/event.html b/src/addons/calendar/pages/event/event.html index 493b41712..3874350f4 100644 --- a/src/addons/calendar/pages/event/event.html +++ b/src/addons/calendar/pages/event/event.html @@ -123,34 +123,26 @@ - + -

- {{ 'core.defaultvalue' | translate :{$a: ((event.timestart - defaultTime) * 1000) | coreFormatDate } }} -

-

{{ reminder.time * 1000 | coreFormatDate }}

+

{{ reminder.label }}

+ [attr.aria-label]="'core.delete' | translate" slot="end" *ngIf="reminder.timestamp > currentTime">
- + - + {{ 'addon.calendar.setnewreminder' | translate }} - diff --git a/src/addons/calendar/pages/event/event.page.ts b/src/addons/calendar/pages/event/event.page.ts index 9ddc41f58..bb21b880d 100644 --- a/src/addons/calendar/pages/event/event.page.ts +++ b/src/addons/calendar/pages/event/event.page.ts @@ -20,7 +20,7 @@ import { AddonCalendarEventToDisplay, AddonCalendarProvider, } from '../../services/calendar'; -import { AddonCalendarHelper } from '../../services/calendar-helper'; +import { AddonCalendarEventReminder, AddonCalendarHelper } from '../../services/calendar-helper'; import { AddonCalendarOffline } from '../../services/calendar-offline'; import { AddonCalendarSync, AddonCalendarSyncEvents, AddonCalendarSyncProvider } from '../../services/calendar-sync'; import { CoreApp } from '@services/app'; @@ -36,10 +36,9 @@ import { Network, NgZone, Translate } from '@singletons'; import { Subscription } from 'rxjs'; import { CoreNavigator } from '@services/navigator'; import { CoreUtils } from '@services/utils/utils'; -import { AddonCalendarReminderDBRecord } from '../../services/database/calendar'; import { ActivatedRoute } from '@angular/router'; import { CoreConstants } from '@/core/constants'; -import { CoreLang } from '@services/lang'; +import { AddonCalendarReminderTimeModalComponent } from '@addons/calendar/components/reminder-time-modal/reminder-time-modal'; /** * Page that displays a single calendar event. @@ -57,13 +56,11 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { protected syncObserver: CoreEventObserver; protected manualSyncObserver: CoreEventObserver; protected onlineObserver: Subscription; + protected defaultTimeChangedObserver: CoreEventObserver; protected currentSiteId: string; + protected updateCurrentTime?: number; eventLoaded = false; - notificationFormat?: string; - notificationMin?: string; - notificationMax?: string; - notificationTimeText?: string; event?: AddonCalendarEventToDisplay; courseId?: number; courseName = ''; @@ -72,19 +69,16 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { notificationsEnabled = false; moduleUrl = ''; categoryPath = ''; - currentTime?: number; - defaultTime = 0; - reminders: AddonCalendarReminderDBRecord[] = []; + currentTime = -1; + reminders: AddonCalendarEventReminder[] = []; canEdit = false; hasOffline = false; isOnline = false; syncIcon = CoreConstants.ICON_LOADING; // Sync icon. - monthNames?: string[]; constructor( protected route: ActivatedRoute, ) { - this.notificationsEnabled = CoreLocalNotifications.isAvailable(); this.siteHomeId = CoreSites.getCurrentSiteHomeId(); this.currentSiteId = CoreSites.getCurrentSiteId(); @@ -121,21 +115,35 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { this.isOnline = CoreApp.isOnline(); }); }); + + // Reload reminders if default notification time changes. + this.defaultTimeChangedObserver = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => { + this.loadReminders(); + + if (this.event) { + AddonCalendar.scheduleEventsNotifications([this.event]); + } + }, this.currentSiteId); + + // Set and update current time. Use a 5 seconds error margin. + this.currentTime = CoreTimeUtils.timestamp(); + this.updateCurrentTime = window.setInterval(() => { + this.currentTime = CoreTimeUtils.timestamp(); + }, 5000); } - protected async initReminders(): Promise { - if (this.notificationsEnabled) { - this.monthNames = CoreLang.getMonthNames(); - - this.reminders = await AddonCalendar.getEventReminders(this.eventId); - this.defaultTime = await AddonCalendar.getDefaultNotificationTime() * 60; - - // Calculate format to use. - this.notificationFormat = - CoreTimeUtils.fixFormatForDatetime(CoreTimeUtils.convertPHPToMoment( - Translate.instant('core.strftimedatetime'), - )); + /** + * Load reminders. + * + * @return Promise resolved when done. + */ + protected async loadReminders(): Promise { + if (!this.notificationsEnabled || !this.event) { + return; } + + const reminders = await AddonCalendar.getEventReminders(this.eventId, this.currentSiteId); + this.reminders = await AddonCalendarHelper.formatReminders(reminders, this.event.timestart, this.currentSiteId); } /** @@ -155,7 +163,6 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { this.syncIcon = CoreConstants.ICON_LOADING; this.fetchEvent(); - this.initReminders(); } /** @@ -209,6 +216,10 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { const event = await AddonCalendar.getEventById(this.eventId); this.event = await AddonCalendarHelper.formatEventData(event); + // Load reminders, and re-schedule them if needed (maybe the event time has changed). + this.loadReminders(); + AddonCalendar.scheduleEventsNotifications([this.event]); + try { const offlineEvent = AddonCalendarHelper.formatOfflineEventData( await AddonCalendarOffline.getEvent(this.eventId), @@ -223,29 +234,21 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { this.hasOffline = false; } - this.currentTime = CoreTimeUtils.timestamp(); - this.notificationMin = CoreTimeUtils.userDate(this.currentTime * 1000, 'YYYY-MM-DDTHH:mm', false); - this.notificationMax = CoreTimeUtils.userDate( - (this.event!.timestart + this.event!.timeduration) * 1000, - 'YYYY-MM-DDTHH:mm', - false, - ); - // Reset some of the calculated data. this.categoryPath = ''; this.courseName = ''; this.courseUrl = ''; this.moduleUrl = ''; - if (this.event!.moduleIcon) { + if (this.event.moduleIcon) { // It's a module event, translate the module name to the current language. - const name = CoreCourse.translateModuleName(this.event!.modulename || ''); + const name = CoreCourse.translateModuleName(this.event.modulename || ''); if (name.indexOf('core.mod_') === -1) { - this.event!.modulename = name; + this.event.modulename = name; } // Get the module URL. - this.moduleUrl = this.event!.url || ''; + this.moduleUrl = this.event.url || ''; } const promises: Promise[] = []; @@ -310,22 +313,23 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { /** * Add a reminder for this event. */ - async addNotificationTime(): Promise { - if (this.notificationTimeText && this.event && this.event.id) { - let notificationTime = CoreTimeUtils.convertToTimestamp(this.notificationTimeText); - - const currentTime = CoreTimeUtils.timestamp(); - const minute = Math.floor(currentTime / 60) * 60; - - // Check if the notification time is in the same minute as we are, so the notification is triggered. - if (notificationTime >= minute && notificationTime < minute + 60) { - notificationTime = currentTime + 1; - } - - await AddonCalendar.addEventReminder(this.event, notificationTime); - this.reminders = await AddonCalendar.getEventReminders(this.eventId); - this.notificationTimeText = undefined; + async addReminder(): Promise { + if (!this.event || !this.event.id) { + return; } + + const reminderTime = await CoreDomUtils.openModal({ + component: AddonCalendarReminderTimeModalComponent, + }); + + if (reminderTime === undefined) { + // User canceled. + return; + } + + await AddonCalendar.addEventReminder(this.event, reminderTime, this.currentSiteId); + + await this.loadReminders(); } /** @@ -345,7 +349,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { try { await AddonCalendar.deleteEventReminder(id); - this.reminders = await AddonCalendar.getEventReminders(this.eventId); + await this.loadReminders(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error deleting reminder'); } finally { @@ -549,6 +553,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { this.syncObserver?.off(); this.manualSyncObserver?.off(); this.onlineObserver?.unsubscribe(); + clearInterval(this.updateCurrentTime); } } diff --git a/src/addons/calendar/pages/settings/settings.html b/src/addons/calendar/pages/settings/settings.html index 8cc5f6dee..8ebb4f579 100644 --- a/src/addons/calendar/pages/settings/settings.html +++ b/src/addons/calendar/pages/settings/settings.html @@ -8,18 +8,10 @@ - + {{ 'addon.calendar.defaultnotificationtime' | translate }} - - {{ 'core.settings.disabled' | translate }} - {{ 600 | coreDuration }} - {{ 1800 | coreDuration }} - {{ 3600 | coreDuration }} - {{ 7200 | coreDuration }} - {{ 21600 | coreDuration }} - {{ 43200 | coreDuration }} - {{ 86400 | coreDuration }} + + {{ defaultTimeLabel }} diff --git a/src/addons/calendar/pages/settings/settings.ts b/src/addons/calendar/pages/settings/settings.ts index a42184f2d..ec1ee3455 100644 --- a/src/addons/calendar/pages/settings/settings.ts +++ b/src/addons/calendar/pages/settings/settings.ts @@ -13,9 +13,16 @@ // limitations under the License. import { Component, OnInit } from '@angular/core'; -import { AddonCalendar, AddonCalendarProvider } from '../../services/calendar'; +import { + AddonCalendar, + AddonCalendarProvider, + AddonCalendarReminderUnits, + AddonCalendarValueAndUnit, +} from '../../services/calendar'; import { CoreEvents } from '@singletons/events'; import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { AddonCalendarReminderTimeModalComponent } from '@addons/calendar/components/reminder-time-modal/reminder-time-modal'; /** * Page that displays the calendar settings. @@ -26,13 +33,51 @@ import { CoreSites } from '@services/sites'; }) export class AddonCalendarSettingsPage implements OnInit { - defaultTime = -1; + defaultTimeLabel = ''; + + protected defaultTime: AddonCalendarValueAndUnit = { + value: 0, + unit: AddonCalendarReminderUnits.MINUTE, + }; /** * View loaded. */ async ngOnInit(): Promise { - this.defaultTime = await AddonCalendar.getDefaultNotificationTime(); + const defaultTime = await AddonCalendar.getDefaultNotificationTime(); + + this.defaultTime = AddonCalendarProvider.convertSecondsToValueAndUnit(defaultTime); + this.defaultTimeLabel = AddonCalendar.getUnitValueLabel(this.defaultTime.value, this.defaultTime.unit); + } + + /** + * Change default time. + * + * @param e Event. + * @return Promise resolved when done. + */ + async changeDefaultTime(e: Event): Promise { + e.stopPropagation(); + e.stopImmediatePropagation(); + e.preventDefault(); + + const reminderTime = await CoreDomUtils.openModal({ + component: AddonCalendarReminderTimeModalComponent, + componentProps: { + initialValue: this.defaultTime, + allowDisable: true, + }, + }); + + if (reminderTime === undefined) { + // User canceled. + return; + } + + this.defaultTime = AddonCalendarProvider.convertSecondsToValueAndUnit(reminderTime); + this.defaultTimeLabel = AddonCalendar.getUnitValueLabel(this.defaultTime.value, this.defaultTime.unit); + + this.updateDefaultTime(reminderTime); } /** diff --git a/src/addons/calendar/services/calendar-helper.ts b/src/addons/calendar/services/calendar-helper.ts index 447745701..1cf856436 100644 --- a/src/addons/calendar/services/calendar-helper.ts +++ b/src/addons/calendar/services/calendar-helper.ts @@ -23,6 +23,7 @@ import { AddonCalendarEventType, AddonCalendarGetEventsEvent, AddonCalendarProvider, + AddonCalendarReminderUnits, AddonCalendarWeek, AddonCalendarWeekDay, } from './calendar'; @@ -35,6 +36,7 @@ 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'; /** * Context levels enumeration. @@ -282,6 +284,55 @@ export class AddonCalendarHelperProvider { } } + /** + * Format reminders, adding calculated data. + * + * @param reminders Reminders. + * @param timestart Event timestart. + * @param siteId Site ID. + * @return Formatted reminders. + */ + async formatReminders( + reminders: AddonCalendarReminderDBRecord[], + 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; + } + + return formattedReminders.map((reminder) => { + if (reminder.time === null) { + // 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; + } + } else { + const data = AddonCalendarProvider.convertSecondsToValueAndUnit(reminder.time); + reminder.value = data.value; + reminder.unit = data.unit; + reminder.timestamp = eventTimestart - reminder.time; + } + + if (reminder.value && reminder.unit) { + reminder.label = AddonCalendar.getUnitValueLabel(reminder.value, reminder.unit, reminder.time === null); + } + + return reminder; + }); + } + /** * Get options (name & value) for each allowed event type. * @@ -725,3 +776,13 @@ export type AddonCalendarEventTypeOption = { name: string; value: AddonCalendarEventType; }; + +/** + * Formatted event reminder. + */ +export type AddonCalendarEventReminder = AddonCalendarReminderDBRecord & { + value?: number; // Amount of time. + unit?: AddonCalendarReminderUnits; // Units. + timestamp?: number; // Timestamp (in seconds). + label?: string; // Label to represent the reminder. +}; diff --git a/src/addons/calendar/services/calendar.ts b/src/addons/calendar/services/calendar.ts index f21f71869..5ea82fe81 100644 --- a/src/addons/calendar/services/calendar.ts +++ b/src/addons/calendar/services/calendar.ts @@ -53,6 +53,16 @@ export enum AddonCalendarEventType { USER = 'user', } +/** + * Units to set a reminder. + */ +export enum AddonCalendarReminderUnits { + MINUTE = CoreConstants.SECONDS_MINUTE, + HOUR = CoreConstants.SECONDS_HOUR, + DAY = CoreConstants.SECONDS_DAY, + WEEK = CoreConstants.SECONDS_WEEK, +} + declare module '@singletons/events' { /** @@ -72,6 +82,21 @@ declare module '@singletons/events' { } +const REMINDER_UNITS_LABELS = { + single: { + [AddonCalendarReminderUnits.MINUTE]: 'core.minute', + [AddonCalendarReminderUnits.HOUR]: 'core.hour', + [AddonCalendarReminderUnits.DAY]: 'core.day', + [AddonCalendarReminderUnits.WEEK]: 'core.week', + }, + multi: { + [AddonCalendarReminderUnits.MINUTE]: 'core.minutes', + [AddonCalendarReminderUnits.HOUR]: 'core.hours', + [AddonCalendarReminderUnits.DAY]: 'core.days', + [AddonCalendarReminderUnits.WEEK]: 'core.weeks', + }, +}; + /** * Service to handle calendar events. */ @@ -82,7 +107,7 @@ export class AddonCalendarProvider { static readonly COMPONENT = 'AddonCalendarEvents'; static readonly DEFAULT_NOTIFICATION_TIME_CHANGED = 'AddonCalendarDefaultNotificationTimeChangedEvent'; static readonly DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime'; - static readonly DEFAULT_NOTIFICATION_TIME = 60; + static readonly DEFAULT_NOTIFICATION_TIME = 3600; static readonly STARTING_WEEK_DAY = 'addon_calendar_starting_week_day'; static readonly NEW_EVENT_EVENT = 'addon_calendar_new_event'; static readonly NEW_EVENT_DISCARDED_EVENT = 'addon_calendar_new_event_discarded'; @@ -156,6 +181,41 @@ export class AddonCalendarProvider { return !!site?.isVersionGreaterEqualThan('3.7.1'); } + /** + * Given a number of seconds, convert it to a unit&value format compatible with reminders. + * + * @param seconds Number of seconds. + * @return Value and unit. + */ + static convertSecondsToValueAndUnit(seconds: number): AddonCalendarValueAndUnit { + if (seconds <= 0) { + return { + value: 0, + unit: AddonCalendarReminderUnits.MINUTE, + }; + } else if (seconds % AddonCalendarReminderUnits.WEEK === 0) { + return { + value: seconds / AddonCalendarReminderUnits.WEEK, + unit: AddonCalendarReminderUnits.WEEK, + }; + } else if (seconds % AddonCalendarReminderUnits.DAY === 0) { + return { + value: seconds / AddonCalendarReminderUnits.DAY, + unit: AddonCalendarReminderUnits.DAY, + }; + } else if (seconds % AddonCalendarReminderUnits.HOUR === 0) { + return { + value: seconds / AddonCalendarReminderUnits.HOUR, + unit: AddonCalendarReminderUnits.HOUR, + }; + } else { + return { + value: seconds / AddonCalendarReminderUnits.MINUTE, + unit: AddonCalendarReminderUnits.MINUTE, + }; + } + } + /** * Delete an event. * @@ -248,7 +308,7 @@ export class AddonCalendarProvider { REMINDERS_TABLE, { eventid: eventId }, ).then((reminders) => - Promise.all(reminders.map((reminder) => this.deleteEventReminder(reminder.id!, siteId))))); + Promise.all(reminders.map((reminder) => this.deleteEventReminder(reminder.id, siteId))))); try { await Promise.all(promises); @@ -548,7 +608,7 @@ export class AddonCalendarProvider { * Get the configured default notification time. * * @param siteId ID of the site. If not defined, use current site. - * @return Promise resolved with the default time. + * @return Promise resolved with the default time (in seconds). */ async getDefaultNotificationTime(siteId?: string): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); @@ -678,24 +738,27 @@ export class AddonCalendarProvider { /** * Adds an event reminder and schedule a new notification. * - * @param event Event to update its notification time. - * @param time New notification setting timestamp. + * @param event Event to set the reminder. + * @param time 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; timeduration: number; name: string}, - time: number, + time?: number | null, siteId?: string, ): Promise { const site = await CoreSites.getSite(siteId); - const reminder: AddonCalendarReminderDBRecord = { + const reminder: Partial = { eventid: event.id, - time: time, + time: time ?? null, }; + const reminderId = await site.getDb().insertRecord(REMINDERS_TABLE, reminder); - await this.scheduleEventNotification(event, reminderId, time, site.getId()); + const timestamp = time ? event.timestart - time : time; + + await this.scheduleEventNotification(event, reminderId, timestamp, site.getId()); } /** @@ -1022,6 +1085,35 @@ export class AddonCalendarProvider { (categoryId ? categoryId : ''); } + /** + * Given a value and a unit, return the translated label. + * + * @param value Value. + * @param unit Unit. + * @param addDefaultLabel Whether to add the "Default" text. + * @return Translated label. + */ + getUnitValueLabel(value: number, unit: AddonCalendarReminderUnits, addDefaultLabel = false): string { + if (value === 0) { + return Translate.instant('core.settings.disabled'); + } + + const unitsLabel = value === 1 ? + REMINDER_UNITS_LABELS.single[unit] : + REMINDER_UNITS_LABELS.multi[unit]; + + const label = Translate.instant('addon.calendar.timebefore', { + units: Translate.instant(unitsLabel), + value: value, + }); + + if (addDefaultLabel) { + return Translate.instant('core.defaultvalue', { $a: label }); + } + + return label; + } + /** * Get upcoming calendar events. * @@ -1335,14 +1427,14 @@ export class AddonCalendarProvider { * * @param event Event to schedule. * @param reminderId The reminder ID. - * @param time Notification setting time (in minutes). E.g. 10 means "notificate 10 minutes before start". + * @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, + time?: number | null, siteId?: string, ): Promise { @@ -1357,16 +1449,16 @@ export class AddonCalendarProvider { return CoreLocalNotifications.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); } - if (time == -1) { - // If time is -1, get event default time to calculate the notification time. + if (!time) { + // Get event default time to calculate the notification time. time = await this.getDefaultNotificationTime(siteId); - if (time == 0) { + if (time === 0) { // Default notification time is disabled, do not show. return CoreLocalNotifications.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); } - time = event.timestart - (time * 60); + time = event.timestart - time; } time = time * 1000; @@ -1417,17 +1509,18 @@ export class AddonCalendarProvider { siteId = siteId || CoreSites.getCurrentSiteId(); const promises = events.map(async (event) => { - const timeEnd = (event.timestart + event.timeduration) * 1000; - - if (timeEnd <= new Date().getTime()) { - // The event has finished already, don't schedule it. + if (event.timestart * 1000 <= Date.now()) { + // The event has already started, don't schedule it. return this.deleteLocalEvent(event.id, siteId); } const reminders = await this.getEventReminders(event.id, siteId); - const p2 = reminders.map((reminder: AddonCalendarReminderDBRecord) => - this.scheduleEventNotification(event, (reminder.id!), reminder.time, siteId)); + const p2 = reminders.map((reminder) => { + const time = reminder.time ? event.timestart - reminder.time : reminder.time; + + return this.scheduleEventNotification(event, reminder.id, time, siteId); + }); await Promise.all(p2); }); @@ -1467,7 +1560,8 @@ export class AddonCalendarProvider { const reminders = await this.getEventReminders(event.id, siteId); if (reminders.length == 0) { - this.addEventReminder(event, -1, siteId); + // No reminders, create the default one. + this.addEventReminder(event, undefined, siteId); } } @@ -1537,9 +1631,7 @@ export class AddonCalendarProvider { const site = await CoreSites.getSite(siteId); siteId = site.getId(); - await Promise.all(events.map((event: AddonCalendarGetEventsEvent| AddonCalendarCalendarEvent) => - // If event does not exist on the DB, schedule the reminder. - this.storeEventInLocalDb(event, siteId))); + await Promise.all(events.map((event) => this.storeEventInLocalDb(event, siteId))); } /** @@ -2154,3 +2246,11 @@ type AddonCalendarPushNotificationData = { reminderId: number; siteId: string; }; + +/** + * Value and unit for reminders. + */ +export type AddonCalendarValueAndUnit = { + value: number; + unit: AddonCalendarReminderUnits; +}; diff --git a/src/addons/calendar/services/database/calendar.ts b/src/addons/calendar/services/database/calendar.ts index c6d3acd3d..643a5ae98 100644 --- a/src/addons/calendar/services/database/calendar.ts +++ b/src/addons/calendar/services/database/calendar.ts @@ -14,7 +14,8 @@ import { SQLiteDB } from '@classes/sqlitedb'; import { CoreSiteSchema } from '@services/sites'; -import { AddonCalendarEventType } from '../calendar'; +import { CoreUtils } from '@services/utils/utils'; +import { AddonCalendar, AddonCalendarEventType } from '../calendar'; /** * Database variables for AddonDatabase service. @@ -23,7 +24,7 @@ export const EVENTS_TABLE = 'addon_calendar_events_3'; export const REMINDERS_TABLE = 'addon_calendar_reminders'; export const CALENDAR_SITE_SCHEMA: CoreSiteSchema = { name: 'AddonCalendarProvider', - version: 3, + version: 4, canBeCleared: [EVENTS_TABLE], tables: [ { @@ -199,8 +200,9 @@ export const CALENDAR_SITE_SCHEMA: CoreSiteSchema = { ], }, ], - async migrate(db: SQLiteDB, oldVersion: number): Promise { + 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 { @@ -212,6 +214,46 @@ export const CALENDAR_SITE_SCHEMA: CoreSiteSchema = { await db.migrateTable(oldTable, EVENTS_TABLE); } + + if (oldVersion < 4) { + // Migrate reminders. New format @since 4.0. + const defaultTime = await CoreUtils.ignoreErrors(AddonCalendar.getDefaultNotificationTime(siteId)); + if (defaultTime) { + // Convert from minutes to seconds. + AddonCalendar.setDefaultNotificationTime(defaultTime * 60, siteId); + } + + const records = await db.getAllRecords(REMINDERS_TABLE); + 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. Delete the reminder. + await db.deleteRecords(REMINDERS_TABLE, { id: record.id }); + + 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, delete it. + await db.deleteRecords(REMINDERS_TABLE, { id: record.id }); + + return; + } else { + record.time = events[record.eventid].timestart - record.time; + } + + return this.insertRecord(REMINDERS_TABLE, record); + })); + } }, }; @@ -257,7 +299,7 @@ export type AddonCalendarEventDBRecord = { }; export type AddonCalendarReminderDBRecord = { - id?: number; + id: number; eventid: number; - time: number; + time: number | null; // Number of seconds before the event, null for default time. }; diff --git a/src/core/lang.json b/src/core/lang.json index 08efb25f7..99182daa6 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -60,6 +60,7 @@ "coursedetails": "Course details", "coursenogroups": "You are not a member of any group of this course.", "currentdevice": "Current device", + "custom": "Custom", "datastoredoffline": "Data stored in the device because it couldn't be sent. It will be sent automatically later.", "date": "Date", "day": "day", @@ -157,6 +158,8 @@ "maxsizeandattachments": "Maximum file size: {{$a.size}}, maximum number of files: {{$a.attachments}}", "min": "min", "mins": "mins", + "minute": "minute", + "minutes": "minutes", "misc": "Miscellaneous", "mod_assign": "Assignment", "mod_assignment": "Assignment 2.2 (Disabled)", @@ -330,6 +333,8 @@ "viewprofile": "View profile", "warningofflinedatadeleted": "Offline data from {{component}} '{{name}}' has been deleted. {{error}}", "warnopeninbrowser": "

You are about to leave the app to open the following URL in your device's browser. Do you want to continue?

\n

{{url}}

", + "week": "week", + "weeks": "weeks", "whatisyourage": "What is your age?", "wheredoyoulive": "In which country do you live?", "whoissiteadmin": "\"Site Administrators\" are the people who manage the Moodle at your school/university/company or learning organisation. If you don't know how to contact them, please contact your teachers/trainers.",