From e86d49742a5acd749bccdb6dd7b40a6a80140e7f Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 15 Nov 2021 14:29:51 +0100 Subject: [PATCH] MOBILE-3909 calendar: Allow setting reminders when creating event --- .../calendar/pages/edit-event/edit-event.html | 26 ++++ .../pages/edit-event/edit-event.page.ts | 79 +++++++++++- .../calendar/services/calendar-offline.ts | 6 +- src/addons/calendar/services/calendar.ts | 122 ++++++++++++------ src/core/services/local-notifications.ts | 12 +- 5 files changed, 201 insertions(+), 44 deletions(-) diff --git a/src/addons/calendar/pages/edit-event/edit-event.html b/src/addons/calendar/pages/edit-event/edit-event.html index 859291287..eb12ca46b 100644 --- a/src/addons/calendar/pages/edit-event/edit-event.html +++ b/src/addons/calendar/pages/edit-event/edit-event.html @@ -225,6 +225,32 @@ + + + + + +

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

+
+
+ + +

{{ reminder.label }}

+
+ + + +
+ + + + + {{ 'addon.calendar.setnewreminder' | translate }} + + + +
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 1ea01493d..1d9b23c0d 100644 --- a/src/addons/calendar/pages/edit-event/edit-event.page.ts +++ b/src/addons/calendar/pages/edit-event/edit-event.page.ts @@ -33,7 +33,7 @@ import { AddonCalendarSubmitCreateUpdateFormDataWSParams, } from '../../services/calendar'; import { AddonCalendarOffline } from '../../services/calendar-offline'; -import { AddonCalendarEventTypeOption, AddonCalendarHelper } from '../../services/calendar-helper'; +import { AddonCalendarEventReminder, AddonCalendarEventTypeOption, AddonCalendarHelper } from '../../services/calendar-helper'; import { AddonCalendarSync, AddonCalendarSyncProvider } from '../../services/calendar-sync'; import { CoreSite } from '@classes/site'; import { Translate } from '@singletons'; @@ -43,6 +43,8 @@ import { CoreError } from '@classes/errors/error'; import { CoreNavigator } from '@services/navigator'; import { CanLeave } from '@guards/can-leave'; import { CoreForms } from '@singletons/form'; +import { CoreLocalNotifications } from '@services/local-notifications'; +import { AddonCalendarReminderTimeModalComponent } from '@addons/calendar/components/reminder-time-modal/reminder-time-modal'; /** * Page that displays a form to create/edit an event. @@ -83,6 +85,10 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { groupControl: FormControl; descriptionControl: FormControl; + // Reminders. + notificationsEnabled = false; + reminders: AddonCalendarEventCandidateReminder[] = []; + protected courseId!: number; protected originalData?: AddonCalendarOfflineEventDBRecord; protected currentSite: CoreSite; @@ -95,6 +101,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { protected fb: FormBuilder, ) { this.currentSite = CoreSites.getRequiredCurrentSite(); + this.notificationsEnabled = CoreLocalNotifications.isAvailable(); this.errors = { required: Translate.instant('core.required'), }; @@ -140,6 +147,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { this.form.addControl('timedurationuntil', this.fb.control(currentDate)); this.form.addControl('courseid', this.fb.control(this.courseId)); + this.initReminders(); this.fetchData().finally(() => { this.originalData = CoreUtils.clone(this.form.value); this.loaded = true; @@ -519,7 +527,9 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { let event: AddonCalendarEvent | AddonCalendarOfflineEventDBRecord; try { - const result = await AddonCalendar.submitEvent(this.eventId, data); + const result = await AddonCalendar.submitEvent(this.eventId, data, { + reminders: this.reminders, + }); event = result.event; CoreForms.triggerFormSubmittedEvent(this.formElement, result.sent, this.currentSite.getId()); @@ -630,6 +640,69 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { } } + /** + * Init reminders. + * + * @return Promise resolved when done. + */ + protected async initReminders(): Promise { + if (!this.notificationsEnabled) { + return; + } + + // Check if default reminders are enabled. + const defaultTime = await AddonCalendar.getDefaultNotificationTime(this.currentSite.getId()); + if (defaultTime === 0) { + return; + } + + const data = AddonCalendarProvider.convertSecondsToValueAndUnit(defaultTime); + + // Add default reminder. + this.reminders.push({ + time: null, + value: data.value, + unit: data.unit, + label: AddonCalendar.getUnitValueLabel(data.value, data.unit, true), + }); + } + + /** + * Add a reminder. + */ + async addReminder(): Promise { + const reminderTime = await CoreDomUtils.openModal({ + component: AddonCalendarReminderTimeModalComponent, + }); + + if (reminderTime === undefined) { + // User canceled. + return; + } + + const data = AddonCalendarProvider.convertSecondsToValueAndUnit(reminderTime); + + // Add reminder. + this.reminders.push({ + time: reminderTime, + value: data.value, + unit: data.unit, + label: AddonCalendar.getUnitValueLabel(data.value, data.unit), + }); + } + + /** + * Remove a reminder. + * + * @param reminder The reminder to remove. + */ + removeReminder(reminder: AddonCalendarEventCandidateReminder): void { + const index = this.reminders.indexOf(reminder); + if (index != -1) { + this.reminders.splice(index, 1); + } + } + /** * Page destroyed. */ @@ -639,3 +712,5 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { } } + +type AddonCalendarEventCandidateReminder = Omit; diff --git a/src/addons/calendar/services/calendar-offline.ts b/src/addons/calendar/services/calendar-offline.ts index e34ea7eb7..5d11344fa 100644 --- a/src/addons/calendar/services/calendar-offline.ts +++ b/src/addons/calendar/services/calendar-offline.ts @@ -215,20 +215,18 @@ export class AddonCalendarOfflineProvider { /** * Offline version for adding a new discussion to a forum. * - * @param eventId Event ID. If it's a new event, set it to undefined/null. + * @param eventId Event ID. Negative value to edit offline event. If it's a new event, set it to undefined/null. * @param data Event data. - * @param timeCreated The time the event was created. If not defined, current time. * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the stored event. */ async saveEvent( eventId: number | undefined, data: AddonCalendarSubmitCreateUpdateFormDataWSParams, - timeCreated?: number, siteId?: string, ): Promise { const site = await CoreSites.getSite(siteId); - timeCreated = timeCreated || Date.now(); + const timeCreated = Date.now(); const event: AddonCalendarOfflineEventDBRecord = { id: eventId || -timeCreated, name: data.name, diff --git a/src/addons/calendar/services/calendar.ts b/src/addons/calendar/services/calendar.ts index 93780709c..815254572 100644 --- a/src/addons/calendar/services/calendar.ts +++ b/src/addons/calendar/services/calendar.ts @@ -744,7 +744,7 @@ export class AddonCalendarProvider { * @return Promise resolved when the notification is updated. */ async addEventReminder( - event: { id: number; timestart: number; timeduration: number; name: string}, + event: { id: number; timestart: number; name: string}, time?: number | null, siteId?: string, ): Promise { @@ -836,7 +836,7 @@ export class AddonCalendarProvider { preSets.emergencyCache = false; } const response: AddonCalendarCalendarDay = await site.read('core_calendar_get_calendar_day_view', params, preSets); - this.storeEventsInLocalDB(response.events, siteId); + this.storeEventsInLocalDB(response.events, { siteId }); return response; } @@ -1040,7 +1040,7 @@ export class AddonCalendarProvider { const response = await site.read('core_calendar_get_calendar_monthly_view', params, preSets); response.weeks.forEach((week) => { week.days.forEach((day) => { - this.storeEventsInLocalDB(day.events as AddonCalendarCalendarEvent[], siteId); + this.storeEventsInLocalDB(day.events as AddonCalendarCalendarEvent[], { siteId }); }); }); @@ -1153,7 +1153,7 @@ export class AddonCalendarProvider { } const response = await site.read('core_calendar_get_calendar_upcoming_view', params, preSets); - this.storeEventsInLocalDB(response.events, siteId); + this.storeEventsInLocalDB(response.events, { siteId }); return response; } @@ -1548,21 +1548,29 @@ export class AddonCalendarProvider { * Store an event in local DB as it is. * * @param event Event to store. - * @param siteId ID of the site the event belongs to. If not defined, use current site. + * @param options Options. * @return Promise resolved when stored. */ - async storeEventInLocalDb(event: AddonCalendarGetEventsEvent | AddonCalendarCalendarEvent, siteId?: string): Promise { - const site = await CoreSites.getSite(siteId); - siteId = site.getId(); - try { - await this.getEventFromLocalDb(event.id, site.id); - } catch { - // Event does not exist. Check if any reminder exists first. - const reminders = await this.getEventReminders(event.id, siteId); + protected async storeEventInLocalDb( + event: AddonCalendarGetEventsEvent | AddonCalendarCalendarEvent | AddonCalendarEvent, + options: AddonCalendarStoreEventsOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + const siteId = site.getId(); + const addDefaultReminder = options.addDefaultReminder ?? true; - if (reminders.length == 0) { - // No reminders, create the default one. - this.addEventReminder(event, undefined, siteId); + 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); + } } } @@ -1600,12 +1608,17 @@ export class AddonCalendarProvider { viewurl: event.viewurl, isactionevent: event.isactionevent ? 1 : 0, url: event.url, - islastday: event.islastday ? 1 : 0, - popupname: event.popupname, - mindaytimestamp: event.mindaytimestamp, - maxdaytimestamp: event.maxdaytimestamp, - draggable: event.draggable ? 1 : 0, }); + + if ('islastday' in event) { + eventRecord = Object.assign(eventRecord, { + islastday: event.islastday ? 1 : 0, + popupname: event.popupname, + mindaytimestamp: event.mindaytimestamp, + maxdaytimestamp: event.maxdaytimestamp, + draggable: event.draggable ? 1 : 0, + }); + } } else if ('uuid' in event) { eventRecord = Object.assign(eventRecord, { courseid: event.courseid, @@ -1622,23 +1635,20 @@ export class AddonCalendarProvider { * Store events in local DB. * * @param events Events to store. - * @param siteId ID of the site the event belongs to. If not defined, use current site. + * @param options Options. * @return Promise resolved when the events are stored. */ protected async storeEventsInLocalDB( - events: (AddonCalendarGetEventsEvent | AddonCalendarCalendarEvent)[], - siteId?: string, + events: (AddonCalendarGetEventsEvent | AddonCalendarCalendarEvent | AddonCalendarEvent)[], + options: AddonCalendarStoreEventsOptions = {}, ): Promise { - const site = await CoreSites.getSite(siteId); - siteId = site.getId(); - - await Promise.all(events.map((event) => this.storeEventInLocalDb(event, siteId))); + await Promise.all(events.map((event) => this.storeEventInLocalDb(event, options))); } /** * Submit a calendar event. * - * @param eventId ID of the event. If undefined/null, create a new event. + * @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. @@ -1648,19 +1658,26 @@ export class AddonCalendarProvider { async submitEvent( eventId: number | undefined, formData: AddonCalendarSubmitCreateUpdateFormDataWSParams, - timeCreated?: number, - forceOffline = false, - siteId?: string, + options: AddonCalendarSubmitEventOptions = {}, ): Promise<{sent: boolean; event: AddonCalendarOfflineEventDBRecord | AddonCalendarEvent}> { - siteId = siteId || CoreSites.getCurrentSiteId(); + const siteId = options.siteId || CoreSites.getCurrentSiteId(); // Function to store the event to be synchronized later. - const storeOffline = (): Promise<{ sent: boolean; event: AddonCalendarOfflineEventDBRecord }> => - AddonCalendarOffline.saveEvent(eventId, formData, timeCreated, siteId).then((event) => - ({ sent: false, event })); + const storeOffline = async (): Promise<{ sent: boolean; event: AddonCalendarOfflineEventDBRecord }> => { + const event = await AddonCalendarOffline.saveEvent(eventId, formData, siteId); - if (forceOffline || !CoreApp.isOnline()) { + // Now save the reminders if any. + if (options.reminders) { + await CoreUtils.ignoreErrors( + Promise.all(options.reminders.map((reminder) => this.addEventReminder(event, reminder.time, siteId))), + ); + } + + return { sent: false, event }; + }; + + if (options.forceOffline || !CoreApp.isOnline()) { // App is offline, store the event. return storeOffline(); } @@ -1672,6 +1689,13 @@ export class AddonCalendarProvider { try { const event = await this.submitEventOnline(eventId, formData, siteId); + // Now save the reminders if any. + if (options.reminders) { + await CoreUtils.ignoreErrors( + Promise.all(options.reminders.map((reminder) => this.addEventReminder(event, reminder.time, siteId))), + ); + } + return ({ sent: true, event }); } catch (error) { if (error && !CoreUtils.isWebServiceError(error)) { @@ -1731,6 +1755,11 @@ export class AddonCalendarProvider { ); } + if (formData.id === 0) { + // Store the new event in local DB. + await CoreUtils.ignoreErrors(this.storeEventInLocalDb(result.event, { addDefaultReminder: false, siteId })); + } + return result.event; } @@ -2267,3 +2296,22 @@ export type AddonCalendarValueAndUnit = { value: number; unit: AddonCalendarReminderUnits; }; + +/** + * Options to pass to submit event. + */ +export type AddonCalendarSubmitEventOptions = { + reminders?: { + time: number | null; + }[]; + forceOffline?: boolean; + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Options to pass to store events in local DB. + */ +export type AddonCalendarStoreEventsOptions = { + addDefaultReminder?: boolean; // Whether to add default reminder for new events with no reminders. Defaults to true. + siteId?: string; // Site ID. If not defined, current site. +}; diff --git a/src/core/services/local-notifications.ts b/src/core/services/local-notifications.ts index c6547f8fe..9b69435e0 100644 --- a/src/core/services/local-notifications.ts +++ b/src/core/services/local-notifications.ts @@ -653,9 +653,19 @@ export class CoreLocalNotificationsProvider { */ async trigger(notification: ILocalNotification): Promise { const db = await this.appDB; + let time = Date.now(); + if (notification.trigger?.at) { + // The type says "at" is a Date, but in Android we can receive timestamps instead. + if (typeof notification.trigger.at === 'number') { + time = notification.trigger.at; + } else { + time = notification.trigger.at.getTime(); + } + } + const entry = { id: notification.id, - at: notification.trigger && notification.trigger.at ? notification.trigger.at.getTime() : Date.now(), + at: time, }; return db.insertRecord(TRIGGERED_TABLE_NAME, entry);