diff --git a/scripts/langindex.json b/scripts/langindex.json index 9d82296e5..6f9b71abf 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", @@ -1944,6 +1947,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", @@ -2284,6 +2289,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/calendar/calendar.ts b/src/addons/calendar/components/calendar/calendar.ts index 5575e917c..a4b2178bc 100644 --- a/src/addons/calendar/components/calendar/calendar.ts +++ b/src/addons/calendar/components/calendar/calendar.ts @@ -94,7 +94,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro this.obsDefaultTimeChange = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => { this.weeks.forEach((week) => { week.days.forEach((day) => { - AddonCalendar.scheduleEventsNotifications(day.eventsFormated!); + AddonCalendar.scheduleEventsNotifications(day.eventsFormated || []); }); }); }, this.currentSiteId); @@ -150,7 +150,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro if (this.weeks) { // Check if there's any change in the filter object. - const changes = this.differ.diff(this.filter!); + const changes = this.differ.diff(this.filter || {}); if (changes) { this.filterEvents(); } @@ -173,8 +173,8 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro this.offlineEvents = AddonCalendarHelper.classifyIntoMonths(events); // Get the IDs of events edited in offline. - const filtered = events.filter((event) => event.id! > 0); - this.offlineEditedEventsIds = filtered.map((event) => event.id!); + const filtered = events.filter((event) => event.id > 0); + this.offlineEditedEventsIds = filtered.map((event) => event.id); return; })); @@ -261,7 +261,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro isPast = day.ispast; if (day.istoday) { - day.eventsFormated!.forEach((event) => { + day.eventsFormated?.forEach((event) => { event.ispast = this.isEventPast(event); }); } @@ -306,8 +306,8 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro this.weeks.forEach((week) => { week.days.forEach((day) => { day.filteredEvents = AddonCalendarHelper.getFilteredEvents( - day.eventsFormated!, - this.filter!, + day.eventsFormated || [], + this.filter, this.categories, ); @@ -466,14 +466,14 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro week.days.forEach((day) => { // Schedule notifications for the events retrieved (only future events will be scheduled). - AddonCalendar.scheduleEventsNotifications(day.eventsFormated!); + AddonCalendar.scheduleEventsNotifications(day.eventsFormated || []); if (monthOfflineEvents || this.deletedEvents.length) { // There is offline data, merge it. if (this.deletedEvents.length) { // Mark as deleted the events that were deleted in offline. - day.eventsFormated!.forEach((event) => { + day.eventsFormated?.forEach((event) => { event.deleted = this.deletedEvents.indexOf(event.id) != -1; }); } @@ -483,10 +483,10 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro day.events = day.events.filter((event) => this.offlineEditedEventsIds.indexOf(event.id) == -1); } - if (monthOfflineEvents && monthOfflineEvents[day.mday]) { + if (monthOfflineEvents && monthOfflineEvents[day.mday] && day.eventsFormated) { // Add the offline events (either new or edited). day.eventsFormated = - AddonCalendarHelper.sortEvents(day.eventsFormated!.concat(monthOfflineEvents[day.mday])); + AddonCalendarHelper.sortEvents(day.eventsFormated.concat(monthOfflineEvents[day.mday])); } } }); @@ -505,7 +505,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro this.weeks.forEach((week) => { week.days.forEach((day) => { - const event = day.eventsFormated!.find((event) => event.id == eventId); + const event = day.eventsFormated?.find((event) => event.id == eventId); if (event) { event.deleted = false; @@ -521,7 +521,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro * @return True if it's in the past. */ protected isEventPast(event: { timestart: number; timeduration: number}): boolean { - return (event.timestart + event.timeduration) < this.currentTime!; + return (event.timestart + event.timeduration) < (this.currentTime || CoreTimeUtils.timestamp()); } /** 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/components/upcoming-events/upcoming-events.ts b/src/addons/calendar/components/upcoming-events/upcoming-events.ts index 7566c6d14..f59363845 100644 --- a/src/addons/calendar/components/upcoming-events/upcoming-events.ts +++ b/src/addons/calendar/components/upcoming-events/upcoming-events.ts @@ -106,7 +106,7 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On */ ngDoCheck(): void { // Check if there's any change in the filter object. - const changes = this.differ.diff(this.filter!); + const changes = this.differ.diff(this.filter || {}); if (changes) { this.filterEvents(); } @@ -183,7 +183,7 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On // Re-calculate the formatted time so it uses the device date. const promises = this.events.map((event) => - AddonCalendar.formatEventTime(event, this.timeFormat!).then((time) => { + AddonCalendar.formatEventTime(event, this.timeFormat).then((time) => { event.formattedtime = time; return; @@ -221,7 +221,7 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On * Filter events based on the filter popover. */ protected filterEvents(): void { - this.filteredEvents = AddonCalendarHelper.getFilteredEvents(this.events, this.filter!, this.categories); + this.filteredEvents = AddonCalendarHelper.getFilteredEvents(this.events, this.filter, this.categories); } /** 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/day/day.page.ts b/src/addons/calendar/pages/day/day.page.ts index f02bc54d1..19963cb62 100644 --- a/src/addons/calendar/pages/day/day.page.ts +++ b/src/addons/calendar/pages/day/day.page.ts @@ -286,7 +286,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.offlineEvents = AddonCalendarHelper.classifyIntoMonths(offlineEvents); // Get the IDs of events edited in offline. - this.offlineEditedEventsIds = offlineEvents.filter((event) => event.id! > 0).map((event) => event.id!); + this.offlineEditedEventsIds = offlineEvents.filter((event) => event.id > 0).map((event) => event.id); return; })); @@ -361,7 +361,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { const promises = this.events.map((event) => { event.ispast = this.isPastDay || (this.isCurrentDay && this.isEventPast(event)); - return AddonCalendar.formatEventTime(event, this.timeFormat!, true, dayTime).then((time) => { + return AddonCalendar.formatEventTime(event, this.timeFormat, true, dayTime).then((time) => { event.formattedtime = time; return; @@ -521,12 +521,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { * @param eventId Event to load. */ gotoEvent(eventId: number): void { - if (eventId < 0) { - // It's an offline event, go to the edit page. - this.openEdit(eventId); - } else { - CoreNavigator.navigateToSitePath(`/calendar/event/${eventId}`); - } + CoreNavigator.navigateToSitePath(`/calendar/event/${eventId}`); } /** diff --git a/src/addons/calendar/pages/edit-event/edit-event.html b/src/addons/calendar/pages/edit-event/edit-event.html index 859291287..469fc2e7c 100644 --- a/src/addons/calendar/pages/edit-event/edit-event.html +++ b/src/addons/calendar/pages/edit-event/edit-event.html @@ -113,120 +113,129 @@ - - - - - -

{{ 'core.showmore' | translate }}

-

{{ 'core.showless' | translate }}

-
-
- -
- - - -

{{ 'core.description' | translate }}

+ + + + +

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

- + + + +
+ + +

{{ reminder.label }}

+
+ + +
+
- + +
+ + + +

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

+
+
+ + +

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

+
+ +
+ + +

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

+
+ +
+ + + + + + + +

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

+
+ +
+ + {{ 'addon.calendar.durationminutes' | translate }} + + +
+
+ + + + + +

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

+
+ +
-

{{ 'core.location' | translate }}

+

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

- +
+
- -
- - - -

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

-
-
- - -

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

-
- -
- - -

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

-
- -
- - - - - - - -

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

-
- -
- - {{ 'addon.calendar.durationminutes' | translate }} - - -
-
- - - - + +
+ + -

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

+

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

- - - - -

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

+
+ + +

{{ 'addon.calendar.repeateditall' | translate:{$a: otherEventsCount} }}

- - +
- - - -
- - - -

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

-
-
- - -

{{ 'addon.calendar.repeateditall' | translate:{$a: otherEventsCount} }}

-
- -
- - -

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

-
- -
-
-
+ + +

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

+
+ +
+
+ + + +

{{ 'core.description' | translate }}

+
+ +
+ + + + +

{{ 'core.location' | 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 366e07fa9..d7bcfd094 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. @@ -68,7 +70,6 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { groups: CoreGroup[] = []; loadingGroups = false; courseGroupSet = false; - advanced = false; errors: Record; error = false; eventRepeatId?: number; @@ -83,6 +84,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 +100,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 +146,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; @@ -171,17 +178,19 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { if (this.eventId && !this.gotEventData) { // Editing an event, get the event data. Wait for sync first. + const eventId = this.eventId; + promises.push(AddonCalendarSync.waitForSync(AddonCalendarSyncProvider.SYNC_ID).then(async () => { // Do not block if the scope is already destroyed. if (!this.isDestroyed && this.eventId) { - CoreSync.blockOperation(AddonCalendarProvider.COMPONENT, this.eventId); + CoreSync.blockOperation(AddonCalendarProvider.COMPONENT, eventId); } let eventForm: AddonCalendarEvent | AddonCalendarOfflineEventDBRecord | undefined; // Get the event offline data if there's any. try { - eventForm = await AddonCalendarOffline.getEvent(this.eventId!); + eventForm = await AddonCalendarOffline.getEvent(eventId); this.hasOffline = true; } catch { @@ -189,9 +198,9 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { this.hasOffline = false; } - if (this.eventId! > 0) { + if (eventId > 0) { // It's an online event. get its data from server. - const event = await AddonCalendar.getEventById(this.eventId!); + const event = await AddonCalendar.getEventById(eventId); if (!eventForm) { eventForm = event; // Use offline data first. @@ -431,13 +440,6 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { } } - /** - * Show or hide advanced form fields. - */ - toggleAdvanced(): void { - this.advanced = !this.advanced; - } - selectDuration(duration: string): void { this.form.controls.duration.setValue(duration); } @@ -517,7 +519,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()); @@ -562,7 +566,10 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { if (event) { CoreEvents.trigger( AddonCalendarProvider.NEW_EVENT_EVENT, - { eventId: event.id! }, + { + eventId: event.id, + oldEventId: this.eventId, + }, this.currentSite.getId(), ); } else { @@ -578,10 +585,15 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { * Discard an offline saved discussion. */ async discard(): Promise { + if (!this.eventId) { + return; + } + try { await CoreDomUtils.showConfirm(Translate.instant('core.areyousure')); + try { - await AddonCalendarOffline.deleteEvent(this.eventId!); + await AddonCalendarOffline.deleteEvent(this.eventId); CoreForms.triggerFormCancelledEvent(this.formElement, this.currentSite.getId()); @@ -620,6 +632,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. */ @@ -629,3 +704,5 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { } } + +type AddonCalendarEventCandidateReminder = Omit; diff --git a/src/addons/calendar/pages/event/event.html b/src/addons/calendar/pages/event/event.html index 493b41712..e2279bd11 100644 --- a/src/addons/calendar/pages/event/event.html +++ b/src/addons/calendar/pages/event/event.html @@ -21,8 +21,8 @@ [priority]="400" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"> - + - + -

- {{ '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..ac44adc68 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. @@ -53,17 +52,16 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { protected eventId!: number; protected siteHomeId: number; + protected newEventObserver: CoreEventObserver; protected editEventObserver: CoreEventObserver; 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 +70,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(); @@ -94,7 +89,16 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { // Listen for event edited. If current event is edited, reload the data. this.editEventObserver = CoreEvents.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => { - if (data && data.eventId == this.eventId) { + if (data && data.eventId === this.eventId) { + this.eventLoaded = false; + this.refreshEvent(true, false); + } + }, this.currentSiteId); + + // Listen for event created. If user edits the data of a new offline event or it's sent to server, this event is triggered. + this.newEventObserver = CoreEvents.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { + if (this.eventId < 0 && data && (data.eventId === this.eventId || data.oldEventId === this.eventId)) { + this.eventId = data.eventId; this.eventLoaded = false; this.refreshEvent(true, false); } @@ -121,21 +125,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 +173,6 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { this.syncIcon = CoreConstants.ICON_LOADING; this.fetchEvent(); - this.initReminders(); } /** @@ -166,48 +183,22 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { * @return Promise resolved when done. */ async fetchEvent(sync = false, showErrors = false): Promise { - let deleted = false; - this.isOnline = CoreApp.isOnline(); if (sync) { - // Try to synchronize offline events. - try { - const result = await AddonCalendarSync.syncEvents(); - if (result.warnings && result.warnings.length) { - CoreDomUtils.showErrorModal(result.warnings[0]); - } + const deleted = await this.syncEvents(showErrors); - if (result.deleted && result.deleted.indexOf(this.eventId) != -1) { - // This event was deleted during the sync. - deleted = true; - } - - if (result.updated) { - // Trigger a manual sync event. - result.source = 'event'; - - CoreEvents.trigger( - AddonCalendarSyncProvider.MANUAL_SYNCED, - result, - this.currentSiteId, - ); - } - } catch (error) { - if (showErrors) { - CoreDomUtils.showErrorModalDefault(error, 'core.errorsync', true); - } + if (deleted) { + return; } } - if (deleted) { - return; - } - try { // Get the event data. - const event = await AddonCalendar.getEventById(this.eventId); - this.event = await AddonCalendarHelper.formatEventData(event); + if (this.eventId >= 0) { + const event = await AddonCalendar.getEventById(this.eventId); + this.event = await AddonCalendarHelper.formatEventData(event); + } try { const offlineEvent = AddonCalendarHelper.formatOfflineEventData( @@ -216,20 +207,27 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { // There is offline data, apply it. this.hasOffline = true; - - this.event = Object.assign(this.event, offlineEvent); + this.event = Object.assign(this.event || {}, offlineEvent); } catch { // No offline data. this.hasOffline = false; + + if (this.eventId < 0) { + // It's an offline event, but it wasn't found. Shouldn't happen. + CoreDomUtils.showErrorModal('Event not found.'); + CoreNavigator.back(); + + return; + } } - 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, - ); + if (!this.event) { + return; // At this point we should always have the event, adding this check to avoid TS errors. + } + + // Load reminders, and re-schedule them if needed (maybe the event time has changed). + this.loadReminders(); + AddonCalendar.scheduleEventsNotifications([this.event]); // Reset some of the calculated data. this.categoryPath = ''; @@ -237,18 +235,19 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { 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[] = []; + const event = this.event; const courseId = this.event.courseid; if (courseId != this.siteHomeId) { @@ -262,16 +261,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { // If it's a group event, get the name of the group. if (courseId && this.event.groupid) { - promises.push(CoreGroups.getUserGroupsInCourse(courseId).then((groups) => { - const group = groups.find((group) => group.id == this.event!.groupid); - - this.groupName = group ? group.name : ''; - - return; - }).catch(() => { - // Error getting groups, just don't show the group name. - this.groupName = ''; - })); + promises.push(this.loadGroupName(this.event, courseId)); } if (this.event.iscategoryevent && this.event.category) { @@ -286,14 +276,14 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { // Check if event was deleted in offine. promises.push(AddonCalendarOffline.isEventDeleted(this.eventId).then((deleted) => { - this.event!.deleted = deleted; + event.deleted = deleted; return; })); // Re-calculate the formatted time so it uses the device date. promises.push(AddonCalendar.getCalendarTimeFormat().then(async (timeFormat) => { - this.event!.formattedtime = await AddonCalendar.formatEventTime(this.event!, timeFormat); + event.formattedtime = await AddonCalendar.formatEventTime(event, timeFormat); return; })); @@ -308,24 +298,88 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { } /** - * Add a reminder for this event. + * Sync offline events. + * + * @param showErrors Whether to show sync errors to the user. + * @return Promise resolved with boolean: whether event was deleted on sync. */ - async addNotificationTime(): Promise { - if (this.notificationTimeText && this.event && this.event.id) { - let notificationTime = CoreTimeUtils.convertToTimestamp(this.notificationTimeText); + protected async syncEvents(showErrors = false): Promise { + let deleted = false; - 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; + // Try to synchronize offline events. + try { + const result = await AddonCalendarSync.syncEvents(); + if (result.warnings && result.warnings.length) { + CoreDomUtils.showErrorModal(result.warnings[0]); } - await AddonCalendar.addEventReminder(this.event, notificationTime); - this.reminders = await AddonCalendar.getEventReminders(this.eventId); - this.notificationTimeText = undefined; + if (result.deleted && result.deleted.indexOf(this.eventId) != -1) { + // This event was deleted during the sync. + deleted = true; + } else if (this.eventId < 0 && result.offlineIdMap[this.eventId]) { + // Event was created, use the online ID. + this.eventId = result.offlineIdMap[this.eventId]; + } + + if (result.updated) { + // Trigger a manual sync event. + result.source = 'event'; + + CoreEvents.trigger( + AddonCalendarSyncProvider.MANUAL_SYNCED, + result, + this.currentSiteId, + ); + } + } catch (error) { + if (showErrors) { + CoreDomUtils.showErrorModalDefault(error, 'core.errorsync', true); + } } + + return deleted; + } + + /** + * Load group name. + * + * @param event Event. + * @param courseId Course ID. + * @return Promise resolved when done. + */ + protected async loadGroupName(event: AddonCalendarEventToDisplay, courseId: number): Promise { + try { + const groups = await CoreGroups.getUserGroupsInCourse(courseId); + + const group = groups.find((group) => group.id == event.groupid); + this.groupName = group ? group.name : ''; + + } catch { + // Error getting groups, just don't show the group name. + this.groupName = ''; + } + } + + /** + * Add a reminder for this event. + */ + 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 +399,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 { @@ -387,7 +441,9 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { const promises: Promise[] = []; - promises.push(AddonCalendar.invalidateEvent(this.eventId)); + if (this.eventId > 0) { + promises.push(AddonCalendar.invalidateEvent(this.eventId)); + } promises.push(AddonCalendar.invalidateTimeFormat()); await CoreUtils.allPromisesIgnoringErrors(promises); @@ -454,9 +510,14 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { const modal = await CoreDomUtils.showModalLoading('core.sending', true); try { - const sent = await AddonCalendar.deleteEvent(this.event.id, this.event.name, deleteAll); + let onlineEventDeleted = false; + if (this.event.id < 0) { + await AddonCalendarOffline.deleteEvent(this.event.id); + } else { + onlineEventDeleted = await AddonCalendar.deleteEvent(this.event.id, this.event.name, deleteAll); + } - if (sent) { + if (onlineEventDeleted) { // Event deleted, invalidate right days & months. try { await AddonCalendarHelper.refreshAfterChangeEvent(this.event, deleteAll ? this.event.eventcount : 1); @@ -466,12 +527,16 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { } // Trigger an event. - CoreEvents.trigger(AddonCalendarProvider.DELETED_EVENT_EVENT, { - eventId: this.eventId, - sent: sent, - }, CoreSites.getCurrentSiteId()); + if (this.event.id < 0) { + CoreEvents.trigger(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, {}, CoreSites.getCurrentSiteId()); + } else { + CoreEvents.trigger(AddonCalendarProvider.DELETED_EVENT_EVENT, { + eventId: this.eventId, + sent: onlineEventDeleted, + }, CoreSites.getCurrentSiteId()); + } - if (sent) { + if (onlineEventDeleted || this.event.id < 0) { CoreDomUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000); // Event deleted, close the view. @@ -532,11 +597,21 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { // Event was deleted, close the view. CoreNavigator.back(); } else if (data.events && (!isManual || data.source != 'event')) { - const event = data.events.find((ev) => ev.id == this.eventId); + if (this.eventId < 0) { + if (data.offlineIdMap[this.eventId]) { + // Event was created, use the online ID. + this.eventId = data.offlineIdMap[this.eventId]; - if (event) { - this.eventLoaded = false; - this.refreshEvent(); + this.eventLoaded = false; + this.refreshEvent(); + } + } else { + const event = data.events.find((ev) => ev.id == this.eventId); + + if (event) { + this.eventLoaded = false; + this.refreshEvent(); + } } } } @@ -545,10 +620,12 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { * Page destroyed. */ ngOnDestroy(): void { - this.editEventObserver?.off(); - this.syncObserver?.off(); - this.manualSyncObserver?.off(); - this.onlineObserver?.unsubscribe(); + this.editEventObserver.off(); + this.syncObserver.off(); + this.manualSyncObserver.off(); + this.onlineObserver.unsubscribe(); + this.newEventObserver.off(); + clearInterval(this.updateCurrentTime); } } diff --git a/src/addons/calendar/pages/index/index.page.ts b/src/addons/calendar/pages/index/index.page.ts index 7e4f1eeb2..f2c8fb51b 100644 --- a/src/addons/calendar/pages/index/index.page.ts +++ b/src/addons/calendar/pages/index/index.page.ts @@ -301,12 +301,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { * @param eventId Event to load. */ gotoEvent(eventId: number): void { - if (eventId < 0) { - // It's an offline event, go to the edit page. - this.openEdit(eventId); - } else { - CoreNavigator.navigateToSitePath(`/calendar/event/${eventId}`); - } + CoreNavigator.navigateToSitePath(`/calendar/event/${eventId}`); } /** 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..d441b1c1d 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. @@ -220,7 +222,7 @@ export class AddonCalendarHelperProvider { formatOfflineEventData(event: AddonCalendarOfflineEventDBRecord): AddonCalendarEventToDisplay { const eventFormatted: AddonCalendarEventToDisplay = { - id: event.id!, + id: event.id, name: event.name, timestart: event.timestart, eventtype: event.eventtype, @@ -243,6 +245,8 @@ export class AddonCalendarHelperProvider { format: 1, visible: 1, offline: true, + canedit: event.id < 0, + candelete: event.id < 0, timeduration: 0, }; @@ -282,6 +286,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. * @@ -335,7 +388,7 @@ export class AddonCalendarHelperProvider { year: number, month: number, siteId?: string, - ): Promise<{ daynames: Partial[]; weeks: Partial[] }> { + ): Promise<{ daynames: Partial[]; weeks: AddonCalendarWeek[] }> { const site = await CoreSites.getSite(siteId); // Get starting week day user preference, fallback to site configuration. let startWeekDayStr = site.getStoredConfig('calendar_startwday'); @@ -344,7 +397,7 @@ export class AddonCalendarHelperProvider { const today = moment(); const isCurrentMonth = today.year() == year && today.month() == month - 1; - const weeks: Partial[] = []; + const weeks: AddonCalendarWeek[] = []; let date = moment({ year, month: month - 1, date: 1 }); for (let mday = 1; mday <= date.daysInMonth(); mday++) { @@ -371,7 +424,7 @@ export class AddonCalendarHelperProvider { } // Add day to current week. - weeks[weeks.length - 1].days!.push({ + weeks[weeks.length - 1].days.push({ events: [], hasevents: false, mday: date.date(), @@ -450,11 +503,11 @@ export class AddonCalendarHelperProvider { */ getFilteredEvents( events: AddonCalendarEventToDisplay[], - filter: AddonCalendarFilter, + filter: AddonCalendarFilter | undefined, categories: { [id: number]: CoreCategoryData }, ): AddonCalendarEventToDisplay[] { // Do not filter. - if (!filter.filtered) { + if (!filter || !filter.filtered) { return events; } @@ -475,9 +528,9 @@ export class AddonCalendarHelperProvider { * Check if an event should be displayed based on the filter. * * @param event Event object. + * @param categories Categories indexed by ID. * @param courseId Course ID to filter. * @param categoryId Category ID the course belongs to. - * @param categories Categories indexed by ID. * @return Whether it should be displayed. */ protected shouldDisplayEvent( @@ -492,7 +545,7 @@ export class AddonCalendarHelperProvider { } if (event.eventtype == 'category' && categories) { - if (!event.categoryid || !Object.keys(categories).length) { + if (!event.categoryid || !Object.keys(categories).length || !categoryId) { // We can't tell if the course belongs to the category, display them all. return true; } @@ -503,7 +556,7 @@ export class AddonCalendarHelperProvider { } // Check parent categories. - let category = categories[categoryId!]; + let category = categories[categoryId]; while (category) { if (!category.parent) { // Category doesn't have parent, stop. @@ -567,7 +620,7 @@ export class AddonCalendarHelperProvider { await AddonCalendar.getLocalEventsByRepeatIdFromLocalDb(eventData.repeatid, site.id); await CoreUtils.allPromises(repeatedEvents.map((event) => - AddonCalendar.invalidateEvent(event.id!))); + AddonCalendar.invalidateEvent(event.id))); return; } @@ -670,7 +723,7 @@ export class AddonCalendarHelperProvider { */ refreshAfterChangeEvent( event: { - id?: number; + id: number; repeatid?: number; timestart: number; }, @@ -679,7 +732,7 @@ export class AddonCalendarHelperProvider { ): Promise { return this.refreshAfterChangeEvents( [{ - id: event.id!, + id: event.id, repeatid: event.repeatid, timestart: event.timestart, repeated: repeated, @@ -725,3 +778,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-offline.ts b/src/addons/calendar/services/calendar-offline.ts index d34586f91..5d11344fa 100644 --- a/src/addons/calendar/services/calendar-offline.ts +++ b/src/addons/calendar/services/calendar-offline.ts @@ -110,7 +110,7 @@ export class AddonCalendarOfflineProvider { async getAllEditedEventsIds(siteId?: string): Promise { const events = await this.getAllEditedEvents(siteId); - return events.map((event) => event.id!); + return events.map((event) => event.id); } /** @@ -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-sync.ts b/src/addons/calendar/services/calendar-sync.ts index 5d6fcad20..56f3e8a3c 100644 --- a/src/addons/calendar/services/calendar-sync.ts +++ b/src/addons/calendar/services/calendar-sync.ts @@ -100,9 +100,10 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { siteId = siteId || CoreSites.getCurrentSiteId(); - if (this.isSyncing(AddonCalendarSyncProvider.SYNC_ID, siteId)) { + const currentSyncPromise = this.getOngoingSync(AddonCalendarSyncProvider.SYNC_ID, siteId); + if (currentSyncPromise) { // There's already a sync ongoing for this site, return the promise. - return this.getOngoingSync(AddonCalendarSyncProvider.SYNC_ID, siteId)!; + return currentSyncPromise; } this.logger.debug('Try to sync calendar events for site ' + siteId); @@ -123,6 +124,7 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider 0 ? eventId : 0, data, siteId); + const newEvent = await AddonCalendar.submitEventOnline(eventId, data, siteId); result.updated = true; result.events.push(newEvent); + if (eventId < 0) { + result.offlineIdMap[eventId] = newEvent.id; + } // Add data to invalidate. const numberOfRepetitions = data.repeat ? data.repeats : @@ -272,7 +277,7 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider; // Map offline ID with online ID for created events. deleted: number[]; toinvalidate: AddonCalendarSyncInvalidateEvent[]; updated: boolean; diff --git a/src/addons/calendar/services/calendar.ts b/src/addons/calendar/services/calendar.ts index f21f71869..815254572 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); @@ -306,7 +366,7 @@ export class AddonCalendarProvider { */ async formatEventTime( event: AddonCalendarEventToDisplay, - format: string, + format?: string, useCommonWords = true, seenDay?: number, showTime = 0, @@ -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(); @@ -656,18 +716,18 @@ export class AddonCalendarProvider { eventConverted.iscategoryevent = originalEvent.eventtype == AddonCalendarEventType.CATEGORY; eventConverted.normalisedeventtype = this.getEventType(recordAsRecord); try { - eventConverted.category = CoreTextUtils.parseJSON(recordAsRecord.category!); + eventConverted.category = CoreTextUtils.parseJSON(recordAsRecord.category || ''); } catch { // Ignore errors. } try { - eventConverted.course = CoreTextUtils.parseJSON(recordAsRecord.course!); + eventConverted.course = CoreTextUtils.parseJSON(recordAsRecord.course || ''); } catch { // Ignore errors. } try { - eventConverted.subscription = CoreTextUtils.parseJSON(recordAsRecord.subscription!); + eventConverted.subscription = CoreTextUtils.parseJSON(recordAsRecord.subscription || ''); } catch { // Ignore errors. } @@ -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, + event: { id: number; timestart: number; name: string}, + 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()); } /** @@ -773,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; } @@ -855,6 +918,10 @@ export class AddonCalendarProvider { const start = initialTime + (CoreConstants.SECONDS_DAY * daysToStart); const end = start + (CoreConstants.SECONDS_DAY * daysInterval) - 1; + const events = { + courseids: [], + groupids: [], + }; const params: AddonCalendarGetCalendarEventsWSParams = { options: { userevents: true, @@ -862,23 +929,20 @@ export class AddonCalendarProvider { timestart: start, timeend: end, }, - events: { - courseids: [], - groupids: [], - }, + events: events, }; const promises: Promise[] = []; promises.push(CoreCourses.getUserCourses(false, siteId).then((courses) => { - params.events!.courseids = courses.map((course) => course.id); - params.events!.courseids.push(site.getSiteHomeId()); // Add front page. + events.courseids = courses.map((course) => course.id); + events.courseids.push(site.getSiteHomeId()); // Add front page. return; })); promises.push(CoreGroups.getAllUserGroups(siteId).then((groups) => { - params.events!.groupids = groups.map((group) => group.id); + events.groupids = groups.map((group) => group.id); return; })); @@ -976,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 }); }); }); @@ -1022,6 +1086,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. * @@ -1060,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; } @@ -1335,14 +1428,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 +1450,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 +1510,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); }); @@ -1454,20 +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) { - this.addEventReminder(event, -1, 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); + } } } @@ -1505,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, @@ -1527,25 +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: 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, 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. @@ -1555,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(); } @@ -1579,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)) { @@ -1594,7 +1711,7 @@ export class AddonCalendarProvider { /** * Submit an event, either to create it or to edit it. It will fail if offline or cannot connect. * - * @param eventId ID of the event. If undefined/null, create a new event. + * @param eventId ID of the event. If undefined/null or negative number, create a new event. * @param formData Form data. * @param siteId Site ID. If not provided, current site. * @return Promise resolved when done. @@ -1605,8 +1722,9 @@ export class AddonCalendarProvider { siteId?: string, ): Promise { const site = await CoreSites.getSite(siteId); + // Add data that is "hidden" in web. - formData.id = eventId; + formData.id = eventId > 0 ? eventId : 0; formData.userid = site.getUserId(); formData.visible = 1; formData.instance = 0; @@ -1615,12 +1733,14 @@ export class AddonCalendarProvider { } else { formData['_qf__core_calendar_local_event_forms_create'] = 1; } + const params: AddonCalendarSubmitCreateUpdateFormWSParams = { formdata: CoreUtils.objectToGetParams(formData), }; const result = await site.write('core_calendar_submit_create_update_form', params); - if (result.validationerror) { + + if (result.validationerror || !result.event) { // Simulate a WS error. throw new CoreWSError({ message: Translate.instant('core.invalidformdata'), @@ -1628,7 +1748,19 @@ export class AddonCalendarProvider { }); } - return result.event!; + if (eventId < 0) { + // Offline event has been sent. Change reminders eventid if any. + await CoreUtils.ignoreErrors( + site.getDb().updateRecords(REMINDERS_TABLE, { eventid: result.event.id }, { eventid: eventId }), + ); + } + + if (formData.id === 0) { + // Store the new event in local DB. + await CoreUtils.ignoreErrors(this.storeEventInLocalDb(result.event, { addDefaultReminder: false, siteId })); + } + + return result.event; } } @@ -2088,7 +2220,8 @@ type AddonCalendarSubmitCreateUpdateFormWSParams = { /** * Form data on AddonCalendarSubmitCreateUpdateFormWSParams. */ -export type AddonCalendarSubmitCreateUpdateFormDataWSParams = Omit & { +export type AddonCalendarSubmitCreateUpdateFormDataWSParams = Omit & { + id?: number; description?: { text: string; format: number; @@ -2143,6 +2276,7 @@ export type AddonCalendarEventToDisplay = Partial & */ export type AddonCalendarUpdatedEventEvent = { eventId: number; + oldEventId?: number; // Old event ID. Used when an offline event is sent. sent?: boolean; }; @@ -2154,3 +2288,30 @@ type AddonCalendarPushNotificationData = { reminderId: number; siteId: string; }; + +/** + * Value and unit for reminders. + */ +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/addons/calendar/services/database/calendar-offline.ts b/src/addons/calendar/services/database/calendar-offline.ts index 046dc59f5..5e08f8378 100644 --- a/src/addons/calendar/services/database/calendar-offline.ts +++ b/src/addons/calendar/services/database/calendar-offline.ts @@ -135,7 +135,7 @@ export const CALENDAR_OFFLINE_SITE_SCHEMA: CoreSiteSchema = { }; export type AddonCalendarOfflineEventDBRecord = { - id?: number; // Negative for offline entries. + id: number; // Negative for offline entries. name: string; timestart: number; eventtype: AddonCalendarEventType; diff --git a/src/addons/calendar/services/database/calendar.ts b/src/addons/calendar/services/database/calendar.ts index c6d3acd3d..9a890b79f 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,11 +214,51 @@ 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); + })); + } }, }; export type AddonCalendarEventDBRecord = { - id?: number; + id: number; name: string; description: string; eventtype: AddonCalendarEventType; @@ -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/addons/messages/services/messages-sync.ts b/src/addons/messages/services/messages-sync.ts index 9a29b9f7d..cdcc4dd6c 100644 --- a/src/addons/messages/services/messages-sync.ts +++ b/src/addons/messages/services/messages-sync.ts @@ -147,9 +147,10 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { siteId = siteId || CoreSites.getCurrentSiteId(); - if (this.isSyncing(assignId, siteId)) { + const currentSyncPromise = this.getOngoingSync(assignId, siteId); + if (currentSyncPromise) { // There's already a sync ongoing for this assign, return the promise. - return this.getOngoingSync(assignId, siteId)!; + return currentSyncPromise; } // Verify that assign isn't blocked. diff --git a/src/addons/mod/choice/services/choice-sync.ts b/src/addons/mod/choice/services/choice-sync.ts index a27a01f05..ed2537467 100644 --- a/src/addons/mod/choice/services/choice-sync.ts +++ b/src/addons/mod/choice/services/choice-sync.ts @@ -122,9 +122,10 @@ export class AddonModChoiceSyncProvider extends CoreCourseActivitySyncBaseProvid siteId = site.getId(); const syncId = this.getSyncId(choiceId, userId); - if (this.isSyncing(syncId, siteId)) { + const currentSyncPromise = this.getOngoingSync(syncId, siteId); + if (currentSyncPromise) { // There's already a sync ongoing for this discussion, return the promise. - return this.getOngoingSync(syncId, siteId)!; + return currentSyncPromise; } this.logger.debug(`Try to sync choice '${choiceId}' for user '${userId}'`); diff --git a/src/addons/mod/data/services/data-sync.ts b/src/addons/mod/data/services/data-sync.ts index a010247c4..9a947ffcb 100644 --- a/src/addons/mod/data/services/data-sync.ts +++ b/src/addons/mod/data/services/data-sync.ts @@ -135,9 +135,10 @@ export class AddonModDataSyncProvider extends CoreCourseActivitySyncBaseProvider syncDatabase(dataId: number, siteId?: string): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); - if (this.isSyncing(dataId, siteId)) { + const currentSyncPromise = this.getOngoingSync(dataId, siteId); + if (currentSyncPromise) { // There's already a sync ongoing for this database, return the promise. - return this.getOngoingSync(dataId, siteId)!; + return currentSyncPromise; } // Verify that database isn't blocked. diff --git a/src/addons/mod/feedback/services/feedback-sync.ts b/src/addons/mod/feedback/services/feedback-sync.ts index d404ed6f2..8b6702b8a 100644 --- a/src/addons/mod/feedback/services/feedback-sync.ts +++ b/src/addons/mod/feedback/services/feedback-sync.ts @@ -130,9 +130,10 @@ export class AddonModFeedbackSyncProvider extends CoreCourseActivitySyncBaseProv syncFeedback(feedbackId: number, siteId?: string): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); - if (this.isSyncing(feedbackId, siteId)) { + const currentSyncPromise = this.getOngoingSync(feedbackId, siteId); + if (currentSyncPromise) { // There's already a sync ongoing for this feedback, return the promise. - return this.getOngoingSync(feedbackId, siteId)!; + return currentSyncPromise; } // Verify that feedback isn't blocked. diff --git a/src/addons/mod/forum/services/forum-sync.ts b/src/addons/mod/forum/services/forum-sync.ts index fae896b0c..65925b3d7 100644 --- a/src/addons/mod/forum/services/forum-sync.ts +++ b/src/addons/mod/forum/services/forum-sync.ts @@ -198,10 +198,11 @@ export class AddonModForumSyncProvider extends CoreCourseActivitySyncBaseProvide siteId = siteId || CoreSites.getCurrentSiteId(); const syncId = this.getForumSyncId(forumId, userId); + const currentSyncPromise = this.getOngoingSync(syncId, siteId); - if (this.isSyncing(syncId, siteId)) { + if (currentSyncPromise) { // There's already a sync ongoing for this discussion, return the promise. - return this.getOngoingSync(syncId, siteId)!; + return currentSyncPromise; } // Verify that forum isn't blocked. @@ -429,10 +430,11 @@ export class AddonModForumSyncProvider extends CoreCourseActivitySyncBaseProvide siteId = siteId || CoreSites.getCurrentSiteId(); const syncId = this.getDiscussionSyncId(discussionId, userId); + const currentSyncPromise = this.getOngoingSync(syncId, siteId); - if (this.isSyncing(syncId, siteId)) { + if (currentSyncPromise) { // There's already a sync ongoing for this discussion, return the promise. - return this.getOngoingSync(syncId, siteId)!; + return currentSyncPromise; } // Verify that forum isn't blocked. diff --git a/src/addons/mod/glossary/services/glossary-sync.ts b/src/addons/mod/glossary/services/glossary-sync.ts index 80a2fbe0d..8d0844cb4 100644 --- a/src/addons/mod/glossary/services/glossary-sync.ts +++ b/src/addons/mod/glossary/services/glossary-sync.ts @@ -144,9 +144,10 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv siteId = siteId || CoreSites.getCurrentSiteId(); const syncId = this.getGlossarySyncId(glossaryId, userId); - if (this.isSyncing(syncId, siteId)) { + const currentSyncPromise = this.getOngoingSync(syncId, siteId); + if (currentSyncPromise) { // There's already a sync ongoing for this glossary, return the promise. - return this.getOngoingSync(syncId, siteId)!; + return currentSyncPromise; } // Verify that glossary isn't blocked. diff --git a/src/addons/mod/h5pactivity/services/h5pactivity-sync.ts b/src/addons/mod/h5pactivity/services/h5pactivity-sync.ts index 10aeb200d..95b5748ca 100644 --- a/src/addons/mod/h5pactivity/services/h5pactivity-sync.ts +++ b/src/addons/mod/h5pactivity/services/h5pactivity-sync.ts @@ -108,9 +108,10 @@ export class AddonModH5PActivitySyncProvider extends CoreCourseActivitySyncBaseP throw new CoreNetworkError(); } - if (this.isSyncing(contextId, siteId)) { + const currentSyncPromise = this.getOngoingSync(contextId, siteId); + if (currentSyncPromise) { // There's already a sync ongoing for this discussion, return the promise. - return this.getOngoingSync(contextId, siteId)!; + return currentSyncPromise; } return this.addOngoingSync(contextId, this.syncActivityData(contextId, siteId), siteId); diff --git a/src/addons/mod/quiz/services/quiz-sync.ts b/src/addons/mod/quiz/services/quiz-sync.ts index 25c101242..6990384f1 100644 --- a/src/addons/mod/quiz/services/quiz-sync.ts +++ b/src/addons/mod/quiz/services/quiz-sync.ts @@ -263,9 +263,10 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider syncQuiz(quiz: AddonModQuizQuizWSData, askPreflight?: boolean, siteId?: string): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); - if (this.isSyncing(quiz.id, siteId)) { + const currentSyncPromise = this.getOngoingSync(quiz.id, siteId); + if (currentSyncPromise) { // There's already a sync ongoing for this quiz, return the promise. - return this.getOngoingSync(quiz.id, siteId)!; + return currentSyncPromise; } // Verify that quiz isn't blocked. diff --git a/src/addons/mod/scorm/services/scorm-sync.ts b/src/addons/mod/scorm/services/scorm-sync.ts index 3ebcfec81..5c9e1f158 100644 --- a/src/addons/mod/scorm/services/scorm-sync.ts +++ b/src/addons/mod/scorm/services/scorm-sync.ts @@ -579,9 +579,10 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide syncScorm(scorm: AddonModScormScorm, siteId?: string): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); - if (this.isSyncing(scorm.id, siteId)) { + const currentSyncPromise = this.getOngoingSync(scorm.id, siteId); + if (currentSyncPromise) { // There's already a sync ongoing for this SCORM, return the promise. - return this.getOngoingSync(scorm.id, siteId)!; + return currentSyncPromise; } // Verify that SCORM isn't blocked. diff --git a/src/addons/mod/survey/services/survey-sync.ts b/src/addons/mod/survey/services/survey-sync.ts index f9c70bf91..e128b0b1e 100644 --- a/src/addons/mod/survey/services/survey-sync.ts +++ b/src/addons/mod/survey/services/survey-sync.ts @@ -124,10 +124,11 @@ export class AddonModSurveySyncProvider extends CoreCourseActivitySyncBaseProvid userId = userId || site.getUserId(); const syncId = this.getSyncId(surveyId, userId); + const currentSyncPromise = this.getOngoingSync(syncId, siteId); - if (this.isSyncing(syncId, siteId)) { + if (currentSyncPromise) { // There's already a sync ongoing for this site, return the promise. - return this.getOngoingSync(syncId, siteId)!; + return currentSyncPromise; } this.logger.debug(`Try to sync survey '${surveyId}' for user '${userId}'`); diff --git a/src/addons/mod/wiki/services/wiki-sync.ts b/src/addons/mod/wiki/services/wiki-sync.ts index 691a08e9b..da3d9ae21 100644 --- a/src/addons/mod/wiki/services/wiki-sync.ts +++ b/src/addons/mod/wiki/services/wiki-sync.ts @@ -167,10 +167,11 @@ export class AddonModWikiSyncProvider extends CoreSyncBaseProvider { siteId = siteId || CoreSites.getCurrentSiteId(); - if (this.isSyncing(workshopId, siteId)) { + const currentSyncPromise = this.getOngoingSync(workshopId, siteId); + if (currentSyncPromise) { // There's already a sync ongoing for this discussion, return the promise. - return this.getOngoingSync(workshopId, siteId)!; + return currentSyncPromise; } // Verify that workshop isn't blocked. diff --git a/src/addons/notes/services/notes-sync.ts b/src/addons/notes/services/notes-sync.ts index a9dfac04d..218b18418 100644 --- a/src/addons/notes/services/notes-sync.ts +++ b/src/addons/notes/services/notes-sync.ts @@ -112,9 +112,10 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider { siteId = siteId || CoreSites.getCurrentSiteId(); - if (this.isSyncing(courseId, siteId)) { + const currentSyncPromise = this.getOngoingSync(courseId, siteId); + if (currentSyncPromise) { // There's already a sync ongoing for notes, return the promise. - return this.getOngoingSync(courseId, siteId)!; + return currentSyncPromise; } this.logger.debug('Try to sync notes for course ' + courseId); diff --git a/src/core/classes/base-sync.ts b/src/core/classes/base-sync.ts index 9c5fc0229..8849ec24c 100644 --- a/src/core/classes/base-sync.ts +++ b/src/core/classes/base-sync.ts @@ -105,7 +105,7 @@ export class CoreSyncBaseProvider { try { return await promise; } finally { - delete this.syncPromises[siteId!][uniqueId]; + delete this.syncPromises[siteId][uniqueId]; } } @@ -133,15 +133,11 @@ export class CoreSyncBaseProvider { */ getOngoingSync(id: string | number, siteId?: string): Promise | undefined { siteId = siteId || CoreSites.getCurrentSiteId(); - - if (!this.isSyncing(id, siteId)) { - return; - } - - // There's already a sync ongoing for this id, return the promise. const uniqueId = this.getUniqueSyncId(id); - return this.syncPromises[siteId][uniqueId]; + if (this.syncPromises[siteId] && this.syncPromises[siteId][uniqueId] !== undefined) { + return this.syncPromises[siteId][uniqueId]; + } } /** @@ -223,11 +219,7 @@ export class CoreSyncBaseProvider { * @return Whether it's synchronizing. */ isSyncing(id: string | number, siteId?: string): boolean { - siteId = siteId || CoreSites.getCurrentSiteId(); - - const uniqueId = this.getUniqueSyncId(id); - - return !!(this.syncPromises[siteId] && this.syncPromises[siteId][uniqueId]); + return !!this.getOngoingSync(id, siteId); } /** @@ -331,10 +323,10 @@ export class CoreSyncBaseProvider { */ protected get componentTranslate(): string { if (!this.componentTranslateInternal) { - this.componentTranslateInternal = Translate.instant(this.componentTranslatableString); + this.componentTranslateInternal = Translate.instant(this.componentTranslatableString); } - return this.componentTranslateInternal!; + return this.componentTranslateInternal; } } diff --git a/src/core/classes/site.ts b/src/core/classes/site.ts index ea5786eca..5dac39fee 100644 --- a/src/core/classes/site.ts +++ b/src/core/classes/site.ts @@ -591,8 +591,7 @@ export class CoreSite { return response; } catch (error) { - if (error.errorcode == 'invalidtoken' || - (error.errorcode == 'accessexception' && error.message.indexOf('Invalid token - token expired') > -1)) { + if (CoreUtils.isExpiredTokenError(error)) { if (initialToken !== this.token && !retrying) { // Token has changed, retry with the new token. preSets.getFromCache = false; // Don't check cache now. Also, it will skip ongoingRequests. diff --git a/src/core/features/comments/services/comments-sync.ts b/src/core/features/comments/services/comments-sync.ts index 1055c8c4b..f402366f8 100644 --- a/src/core/features/comments/services/comments-sync.ts +++ b/src/core/features/comments/services/comments-sync.ts @@ -158,10 +158,11 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { siteId = siteId || CoreSites.getCurrentSiteId(); - if (this.isSyncing(courseId, siteId)) { + const currentSyncPromise = this.getOngoingSync(courseId, siteId); + if (currentSyncPromise) { // There's already a sync ongoing for this discussion, return the promise. - return this.getOngoingSync(courseId, siteId)!; + return currentSyncPromise; } this.logger.debug(`Try to sync course '${courseId}'`); diff --git a/src/core/features/pushnotifications/services/pushnotifications.ts b/src/core/features/pushnotifications/services/pushnotifications.ts index ba07fa9d3..a4aadf0a7 100644 --- a/src/core/features/pushnotifications/services/pushnotifications.ts +++ b/src/core/features/pushnotifications/services/pushnotifications.ts @@ -524,7 +524,13 @@ export class CorePushNotificationsProvider { try { response = await site.write('core_user_remove_user_device', data); } catch (error) { - if (CoreUtils.isWebServiceError(error)) { + if (CoreUtils.isWebServiceError(error) || CoreUtils.isExpiredTokenError(error)) { + // Cannot unregister. Don't try again. + await CoreUtils.ignoreErrors(db.deleteRecords(PENDING_UNREGISTER_TABLE_NAME, { + token: site.getToken(), + siteid: site.getId(), + })); + throw error; } diff --git a/src/core/features/rating/services/rating-sync.ts b/src/core/features/rating/services/rating-sync.ts index 9e6027ca9..09f1af9c2 100644 --- a/src/core/features/rating/services/rating-sync.ts +++ b/src/core/features/rating/services/rating-sync.ts @@ -157,9 +157,10 @@ export class CoreRatingSyncProvider extends CoreSyncBaseProvider { siteId = siteId || CoreSites.getCurrentSiteId(); const syncId = 'preferences'; + const currentSyncPromise = this.getOngoingSync(syncId, siteId); - if (this.isSyncing(syncId, siteId)) { + if (currentSyncPromise) { // There's already a sync ongoing, return the promise. - return this.getOngoingSync(syncId, siteId)!; + return currentSyncPromise; } this.logger.debug('Try to sync user preferences'); 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.", 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); diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 6d06cbf3d..938a48778 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -850,7 +850,7 @@ export class CoreUtilsProvider { } /** - * Given an error returned by a WS call, check if the error is generated by the app or it has been returned by the WebSwervice. + * Given an error returned by a WS call, check if the error is generated by the app or it has been returned by the WebService. * * @param error Error to check. * @return Whether the error was returned by the WebService. @@ -858,10 +858,22 @@ export class CoreUtilsProvider { // eslint-disable-next-line @typescript-eslint/no-explicit-any isWebServiceError(error: any): boolean { return error && (typeof error.warningcode != 'undefined' || (typeof error.errorcode != 'undefined' && - error.errorcode != 'invalidtoken' && error.errorcode != 'userdeleted' && error.errorcode != 'upgraderunning' && + error.errorcode != 'userdeleted' && error.errorcode != 'upgraderunning' && error.errorcode != 'forcepasswordchangenotice' && error.errorcode != 'usernotfullysetup' && error.errorcode != 'sitepolicynotagreed' && error.errorcode != 'sitemaintenance' && - (error.errorcode != 'accessexception' || error.message.indexOf('Invalid token - token expired') == -1))); + !this.isExpiredTokenError(error))); + } + + /** + * Given an error returned by a WS call, check if the error is a token expired error. + * + * @param error Error to check. + * @return Whether the error is a token expired error. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + isExpiredTokenError(error: any): boolean { + return error.errorcode === 'invalidtoken' || + (error.errorcode === 'accessexception' && error.message.includes('Invalid token - token expired')); } /**