commit
bdd6aeb738
|
@ -958,4 +958,46 @@ class behat_app extends behat_app_helper {
|
||||||
$this->getSession()->switchToWindow($windowNames[1]);
|
$this->getSession()->switchToWindow($windowNames[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a notification has been triggered and is present.
|
||||||
|
*
|
||||||
|
* @Then /^a notification with title (".+") is( not)? present in the app$/
|
||||||
|
* @param string $title Notification title
|
||||||
|
* @param bool $not Whether assert that the notification was not found
|
||||||
|
*/
|
||||||
|
public function notification_present_in_the_app(string $title, bool $not = false) {
|
||||||
|
$result = $this->runtime_js("notificationIsPresentWithText($title)");
|
||||||
|
|
||||||
|
if ($not && $result === 'YES') {
|
||||||
|
throw new ExpectationException("Notification is present", $this->getSession()->getDriver());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$not && $result === 'NO') {
|
||||||
|
throw new ExpectationException("Notification is not present", $this->getSession()->getDriver());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($result !== 'YES' && $result !== 'NO') {
|
||||||
|
throw new DriverException('Error checking notification - ' . $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close a notification present in the app
|
||||||
|
*
|
||||||
|
* @Then /^I close a notification with title (".+") in the app$/
|
||||||
|
* @param string $title Notification title
|
||||||
|
*/
|
||||||
|
public function close_notification_app(string $title) {
|
||||||
|
$result = $this->runtime_js("closeNotification($title)");
|
||||||
|
|
||||||
|
if ($result !== 'OK') {
|
||||||
|
throw new DriverException('Error closing notification - ' . $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,7 +147,6 @@
|
||||||
"addon.calendar.sunday": "calendar",
|
"addon.calendar.sunday": "calendar",
|
||||||
"addon.calendar.thu": "calendar",
|
"addon.calendar.thu": "calendar",
|
||||||
"addon.calendar.thursday": "calendar",
|
"addon.calendar.thursday": "calendar",
|
||||||
"addon.calendar.timebefore": "local_moodlemobileapp",
|
|
||||||
"addon.calendar.today": "calendar",
|
"addon.calendar.today": "calendar",
|
||||||
"addon.calendar.tomorrow": "calendar",
|
"addon.calendar.tomorrow": "calendar",
|
||||||
"addon.calendar.tue": "calendar",
|
"addon.calendar.tue": "calendar",
|
||||||
|
@ -161,7 +160,6 @@
|
||||||
"addon.calendar.typeopen": "calendar",
|
"addon.calendar.typeopen": "calendar",
|
||||||
"addon.calendar.typesite": "calendar",
|
"addon.calendar.typesite": "calendar",
|
||||||
"addon.calendar.typeuser": "calendar",
|
"addon.calendar.typeuser": "calendar",
|
||||||
"addon.calendar.units": "qtype_numerical",
|
|
||||||
"addon.calendar.upcomingevents": "calendar",
|
"addon.calendar.upcomingevents": "calendar",
|
||||||
"addon.calendar.userevents": "calendar",
|
"addon.calendar.userevents": "calendar",
|
||||||
"addon.calendar.wed": "calendar",
|
"addon.calendar.wed": "calendar",
|
||||||
|
@ -2154,6 +2152,18 @@
|
||||||
"core.rating.ratings": "rating",
|
"core.rating.ratings": "rating",
|
||||||
"core.redirectingtosite": "local_moodlemobileapp",
|
"core.redirectingtosite": "local_moodlemobileapp",
|
||||||
"core.refresh": "moodle",
|
"core.refresh": "moodle",
|
||||||
|
"core.reminders.atthetime": "local_moodlemobileapp",
|
||||||
|
"core.reminders.custom": "local_moodlemobileapp",
|
||||||
|
"core.reminders.customreminder": "local_moodlemobileapp",
|
||||||
|
"core.reminders.delete": "moodle",
|
||||||
|
"core.reminders.reminderset": "local_moodlemobileapp",
|
||||||
|
"core.reminders.reminderunset": "local_moodlemobileapp",
|
||||||
|
"core.reminders.setareminder": "local_moodlemobileapp",
|
||||||
|
"core.reminders.setareminderfor": "local_moodlemobileapp",
|
||||||
|
"core.reminders.setreminder": "local_moodlemobileapp",
|
||||||
|
"core.reminders.timebefore": "local_moodlemobileapp",
|
||||||
|
"core.reminders.units": "qtype_numerical",
|
||||||
|
"core.reminders.value": "local_moodlemobileapp",
|
||||||
"core.remove": "moodle",
|
"core.remove": "moodle",
|
||||||
"core.removefiles": "local_moodlemobileapp",
|
"core.removefiles": "local_moodlemobileapp",
|
||||||
"core.required": "moodle",
|
"core.required": "moodle",
|
||||||
|
|
|
@ -70,7 +70,7 @@ const mainMenuChildrenRoutes: Routes = [
|
||||||
|
|
||||||
await AddonCalendar.initialize();
|
await AddonCalendar.initialize();
|
||||||
|
|
||||||
AddonCalendar.scheduleAllSitesEventsNotifications();
|
AddonCalendar.updateAllSitesEventReminders();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -19,25 +19,20 @@ import { CoreSharedModule } from '@/core/shared.module';
|
||||||
import { AddonCalendarCalendarComponent } from './calendar/calendar';
|
import { AddonCalendarCalendarComponent } from './calendar/calendar';
|
||||||
import { AddonCalendarUpcomingEventsComponent } from './upcoming-events/upcoming-events';
|
import { AddonCalendarUpcomingEventsComponent } from './upcoming-events/upcoming-events';
|
||||||
import { AddonCalendarFilterComponent } from './filter/filter';
|
import { AddonCalendarFilterComponent } from './filter/filter';
|
||||||
import { AddonCalendarReminderTimeModalComponent } from './reminder-time-modal/reminder-time-modal';
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AddonCalendarCalendarComponent,
|
AddonCalendarCalendarComponent,
|
||||||
AddonCalendarUpcomingEventsComponent,
|
AddonCalendarUpcomingEventsComponent,
|
||||||
AddonCalendarFilterComponent,
|
AddonCalendarFilterComponent,
|
||||||
AddonCalendarReminderTimeModalComponent,
|
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CoreSharedModule,
|
CoreSharedModule,
|
||||||
],
|
],
|
||||||
providers: [
|
|
||||||
],
|
|
||||||
exports: [
|
exports: [
|
||||||
AddonCalendarCalendarComponent,
|
AddonCalendarCalendarComponent,
|
||||||
AddonCalendarUpcomingEventsComponent,
|
AddonCalendarUpcomingEventsComponent,
|
||||||
AddonCalendarFilterComponent,
|
AddonCalendarFilterComponent,
|
||||||
AddonCalendarReminderTimeModalComponent,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AddonCalendarComponentsModule {}
|
export class AddonCalendarComponentsModule {}
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
<ion-header>
|
|
||||||
<ion-toolbar>
|
|
||||||
<ion-title>
|
|
||||||
<h2>{{ 'addon.calendar.reminders' | translate }}</h2>
|
|
||||||
</ion-title>
|
|
||||||
<ion-buttons slot="end">
|
|
||||||
<ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
|
||||||
<ion-icon slot="icon-only" name="fas-times" aria-hidden="true"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</ion-buttons>
|
|
||||||
</ion-toolbar>
|
|
||||||
</ion-header>
|
|
||||||
<ion-content>
|
|
||||||
<form (ngSubmit)="saveReminder()">
|
|
||||||
<ion-radio-group name="radiovalue" [(ngModel)]="radioValue" class="ion-text-wrap">
|
|
||||||
<!-- Preset options. -->
|
|
||||||
<ion-item *ngIf="allowDisable">
|
|
||||||
<ion-label>
|
|
||||||
<p>{{ 'core.settings.disabled' | translate }}</p>
|
|
||||||
</ion-label>
|
|
||||||
<ion-radio slot="end" value="disabled"></ion-radio>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item *ngFor="let option of presetOptions">
|
|
||||||
<ion-label>
|
|
||||||
<p>{{ option.label }}</p>
|
|
||||||
</ion-label>
|
|
||||||
<ion-radio slot="end" [value]="option.radioValue"></ion-radio>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<!-- Custom value. -->
|
|
||||||
<ion-item class="ion-text-wrap">
|
|
||||||
<ion-label>
|
|
||||||
<p>{{ 'core.custom' | translate }}</p>
|
|
||||||
</ion-label>
|
|
||||||
<ion-radio slot="end" value="custom"></ion-radio>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item class="ion-text-wrap" (click)="customInputClicked($event)">
|
|
||||||
<ion-label></ion-label>
|
|
||||||
|
|
||||||
<div class="flex-row">
|
|
||||||
<!-- Input to enter the value. -->
|
|
||||||
<ion-input type="number" name="customvalue" [(ngModel)]="customValue" [disabled]="radioValue != 'custom'"
|
|
||||||
placeholder="10" (click)="customInputClicked($event)">
|
|
||||||
</ion-input>
|
|
||||||
|
|
||||||
<!-- Units. -->
|
|
||||||
<label class="accesshide" for="reminderUnits">{{ 'addon.calendar.units' | translate }}</label>
|
|
||||||
<ion-select id="reminderUnits" name="customunits" [(ngModel)]="customUnits" interface="action-sheet"
|
|
||||||
[disabled]="radioValue != 'custom'" slot="end" [interfaceOptions]="{header: 'addon.calendar.units' | translate}">
|
|
||||||
<ion-select-option *ngFor="let option of customUnitsOptions" [value]="option.value">
|
|
||||||
{{ option.label | translate }}
|
|
||||||
</ion-select-option>
|
|
||||||
</ion-select>
|
|
||||||
</div>
|
|
||||||
</ion-item>
|
|
||||||
</ion-radio-group>
|
|
||||||
|
|
||||||
<ion-button type="submit" class="ion-margin" expand="block" [disabled]="radioValue == 'custom' && !customValue">
|
|
||||||
{{ 'core.done' | translate }}
|
|
||||||
</ion-button>
|
|
||||||
</form>
|
|
||||||
</ion-content>
|
|
|
@ -1,174 +0,0 @@
|
||||||
// (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<void> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom value input clicked.
|
|
||||||
*
|
|
||||||
* @param ev Click event.
|
|
||||||
*/
|
|
||||||
async customInputClicked(ev: Event): Promise<void> {
|
|
||||||
if (this.radioValue === 'custom') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.radioValue = 'custom';
|
|
||||||
|
|
||||||
const target = <HTMLInputElement | HTMLElement | null> ev.target;
|
|
||||||
if (target) {
|
|
||||||
CoreDomUtils.focusElement(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -41,7 +41,6 @@
|
||||||
"noevents": "There are no events",
|
"noevents": "There are no events",
|
||||||
"nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event.",
|
"nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event.",
|
||||||
"reminders": "Reminders",
|
"reminders": "Reminders",
|
||||||
"units": "Units",
|
|
||||||
"repeatedevents": "Repeated events",
|
"repeatedevents": "Repeated events",
|
||||||
"repeateditall": "Also apply changes to the other {{$a}} events in this repeat series",
|
"repeateditall": "Also apply changes to the other {{$a}} events in this repeat series",
|
||||||
"repeateditthis": "Apply changes to this event only",
|
"repeateditthis": "Apply changes to this event only",
|
||||||
|
@ -55,7 +54,6 @@
|
||||||
"sunday": "Sunday",
|
"sunday": "Sunday",
|
||||||
"thu": "Thu",
|
"thu": "Thu",
|
||||||
"thursday": "Thursday",
|
"thursday": "Thursday",
|
||||||
"timebefore": "{{value}} {{units}} before",
|
|
||||||
"today": "Today",
|
"today": "Today",
|
||||||
"tomorrow": "Tomorrow",
|
"tomorrow": "Tomorrow",
|
||||||
"tue": "Tue",
|
"tue": "Tue",
|
||||||
|
|
|
@ -113,7 +113,7 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Reminders. Right now, only allow adding them here for new events. -->
|
<!-- Reminders. Right now, only allow adding them here for new events. -->
|
||||||
<ng-container *ngIf="notificationsEnabled && !eventId">
|
<ng-container *ngIf="remindersEnabled && !eventId">
|
||||||
<ion-item-divider class="addon-calendar-reminders-title">
|
<ion-item-divider class="addon-calendar-reminders-title">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<p class="item-heading">{{ 'addon.calendar.reminders' | translate }}</p>
|
<p class="item-heading">{{ 'addon.calendar.reminders' | translate }}</p>
|
||||||
|
|
|
@ -33,7 +33,7 @@ import {
|
||||||
AddonCalendarSubmitCreateUpdateFormDataWSParams,
|
AddonCalendarSubmitCreateUpdateFormDataWSParams,
|
||||||
} from '../../services/calendar';
|
} from '../../services/calendar';
|
||||||
import { AddonCalendarOffline } from '../../services/calendar-offline';
|
import { AddonCalendarOffline } from '../../services/calendar-offline';
|
||||||
import { AddonCalendarEventReminder, AddonCalendarEventTypeOption, AddonCalendarHelper } from '../../services/calendar-helper';
|
import { AddonCalendarEventTypeOption, AddonCalendarHelper } from '../../services/calendar-helper';
|
||||||
import { AddonCalendarSync, AddonCalendarSyncProvider } from '../../services/calendar-sync';
|
import { AddonCalendarSync, AddonCalendarSyncProvider } from '../../services/calendar-sync';
|
||||||
import { CoreSite } from '@classes/site';
|
import { CoreSite } from '@classes/site';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
|
@ -43,8 +43,8 @@ import { CoreError } from '@classes/errors/error';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CanLeave } from '@guards/can-leave';
|
import { CanLeave } from '@guards/can-leave';
|
||||||
import { CoreForms } from '@singletons/form';
|
import { CoreForms } from '@singletons/form';
|
||||||
import { CoreLocalNotifications } from '@services/local-notifications';
|
import { CoreReminders, CoreRemindersService, CoreRemindersUnits } from '@features/reminders/services/reminders';
|
||||||
import { AddonCalendarReminderTimeModalComponent } from '@addons/calendar/components/reminder-time-modal/reminder-time-modal';
|
import { CoreRemindersSetReminderMenuComponent } from '@features/reminders/components/set-reminder-menu/set-reminder-menu';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays a form to create/edit an event.
|
* Page that displays a form to create/edit an event.
|
||||||
|
@ -85,7 +85,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
|
||||||
descriptionControl: FormControl;
|
descriptionControl: FormControl;
|
||||||
|
|
||||||
// Reminders.
|
// Reminders.
|
||||||
notificationsEnabled = false;
|
remindersEnabled = false;
|
||||||
reminders: AddonCalendarEventCandidateReminder[] = [];
|
reminders: AddonCalendarEventCandidateReminder[] = [];
|
||||||
|
|
||||||
protected courseId!: number;
|
protected courseId!: number;
|
||||||
|
@ -100,7 +100,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
|
||||||
protected fb: FormBuilder,
|
protected fb: FormBuilder,
|
||||||
) {
|
) {
|
||||||
this.currentSite = CoreSites.getRequiredCurrentSite();
|
this.currentSite = CoreSites.getRequiredCurrentSite();
|
||||||
this.notificationsEnabled = CoreLocalNotifications.isAvailable();
|
this.remindersEnabled = CoreReminders.isEnabled();
|
||||||
this.errors = {
|
this.errors = {
|
||||||
required: Translate.instant('core.required'),
|
required: Translate.instant('core.required'),
|
||||||
};
|
};
|
||||||
|
@ -133,7 +133,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.eventId = CoreNavigator.getRouteNumberParam('eventId') || undefined;
|
this.eventId = CoreNavigator.getRouteNumberParam('eventId') || undefined;
|
||||||
|
@ -156,7 +156,6 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
|
||||||
/**
|
/**
|
||||||
* Fetch the data needed to render the form.
|
* Fetch the data needed to render the form.
|
||||||
*
|
*
|
||||||
* @param refresh Whether it's refreshing data.
|
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async fetchData(): Promise<void> {
|
protected async fetchData(): Promise<void> {
|
||||||
|
@ -273,15 +272,14 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const courseFillterFullname = (course: CoreCourseSearchedData | CoreEnrolledCourseData): Promise<void> =>
|
const courseFillFullname = async (course: CoreCourseSearchedData | CoreEnrolledCourseData): Promise<void> => {
|
||||||
CoreFilterHelper.getFiltersAndFormatText(course.fullname, 'course', course.id)
|
try {
|
||||||
.then((result) => {
|
const result = await CoreFilterHelper.getFiltersAndFormatText(course.fullname, 'course', course.id);
|
||||||
course.fullname = result.text;
|
course.fullname = result.text;
|
||||||
|
} catch {
|
||||||
return;
|
// Ignore errors.
|
||||||
}).catch(() => {
|
}
|
||||||
// Ignore errors.
|
};
|
||||||
});
|
|
||||||
|
|
||||||
if (this.showAll) {
|
if (this.showAll) {
|
||||||
// Remove site home from the list of courses.
|
// Remove site home from the list of courses.
|
||||||
|
@ -296,9 +294,9 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
|
||||||
|
|
||||||
// Format the name of the courses.
|
// Format the name of the courses.
|
||||||
if ('contacts' in courses[0]) {
|
if ('contacts' in courses[0]) {
|
||||||
await Promise.all((courses as CoreCourseSearchedData[]).map(courseFillterFullname));
|
await Promise.all((courses as CoreCourseSearchedData[]).map(courseFillFullname));
|
||||||
} else {
|
} else {
|
||||||
await Promise.all((courses as CoreEnrolledCourseData[]).map(courseFillterFullname));
|
await Promise.all((courses as CoreEnrolledCourseData[]).map(courseFillFullname));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort courses by name.
|
// Sort courses by name.
|
||||||
|
@ -461,17 +459,17 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
|
||||||
const timeDurationMinutes = parseInt(formData.timedurationminutes || '', 10);
|
const timeDurationMinutes = parseInt(formData.timedurationminutes || '', 10);
|
||||||
let error: string | undefined;
|
let error: string | undefined;
|
||||||
|
|
||||||
if (formData.eventtype == AddonCalendarEventType.COURSE && !formData.courseid) {
|
if (formData.eventtype === AddonCalendarEventType.COURSE && !formData.courseid) {
|
||||||
error = 'core.selectacourse';
|
error = 'core.selectacourse';
|
||||||
} else if (formData.eventtype == AddonCalendarEventType.GROUP && !formData.groupcourseid) {
|
} else if (formData.eventtype === AddonCalendarEventType.GROUP && !formData.groupcourseid) {
|
||||||
error = 'core.selectacourse';
|
error = 'core.selectacourse';
|
||||||
} else if (formData.eventtype == AddonCalendarEventType.GROUP && !formData.groupid) {
|
} else if (formData.eventtype === AddonCalendarEventType.GROUP && !formData.groupid) {
|
||||||
error = 'core.selectagroup';
|
error = 'core.selectagroup';
|
||||||
} else if (formData.eventtype == AddonCalendarEventType.CATEGORY && !formData.categoryid) {
|
} else if (formData.eventtype === AddonCalendarEventType.CATEGORY && !formData.categoryid) {
|
||||||
error = 'core.selectacategory';
|
error = 'core.selectacategory';
|
||||||
} else if (formData.duration == 1 && timeStartDate > timeUntilDate) {
|
} else if (formData.duration === 1 && timeStartDate > timeUntilDate) {
|
||||||
error = 'addon.calendar.invalidtimedurationuntil';
|
error = 'addon.calendar.invalidtimedurationuntil';
|
||||||
} else if (formData.duration == 2 && (isNaN(timeDurationMinutes) || timeDurationMinutes < 1)) {
|
} else if (formData.duration === 2 && (isNaN(timeDurationMinutes) || timeDurationMinutes < 1)) {
|
||||||
error = 'addon.calendar.invalidtimedurationminutes';
|
error = 'addon.calendar.invalidtimedurationminutes';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -534,8 +532,11 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
|
||||||
|
|
||||||
if (result.sent) {
|
if (result.sent) {
|
||||||
// Event created or edited, invalidate right days & months.
|
// Event created or edited, invalidate right days & months.
|
||||||
const numberOfRepetitions = formData.repeat ? formData.repeats :
|
const numberOfRepetitions = formData.repeat
|
||||||
(data.repeateditall && this.otherEventsCount ? this.otherEventsCount + 1 : 1);
|
? formData.repeats
|
||||||
|
: (data.repeateditall && this.otherEventsCount
|
||||||
|
? this.otherEventsCount + 1
|
||||||
|
: 1);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await AddonCalendarHelper.refreshAfterChangeEvent(result.event, numberOfRepetitions);
|
await AddonCalendarHelper.refreshAfterChangeEvent(result.event, numberOfRepetitions);
|
||||||
|
@ -645,24 +646,23 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
|
||||||
*/
|
*/
|
||||||
protected async initReminders(): Promise<void> {
|
protected async initReminders(): Promise<void> {
|
||||||
// Don't init reminders when editing an event. Right now, only allow adding reminders for new events.
|
// Don't init reminders when editing an event. Right now, only allow adding reminders for new events.
|
||||||
if (!this.notificationsEnabled || this.eventId) {
|
if (!this.remindersEnabled || this.eventId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if default reminders are enabled.
|
// Check if default reminders are enabled.
|
||||||
const defaultTime = await AddonCalendar.getDefaultNotificationTime(this.currentSite.getId());
|
const defaultTime = await CoreReminders.getDefaultNotificationTime(this.currentSite.getId());
|
||||||
if (defaultTime === 0) {
|
if (defaultTime === CoreRemindersService.DISABLED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = AddonCalendarProvider.convertSecondsToValueAndUnit(defaultTime);
|
const data = CoreRemindersService.convertSecondsToValueAndUnit(defaultTime);
|
||||||
|
|
||||||
// Add default reminder.
|
// Add default reminder.
|
||||||
this.reminders.push({
|
this.reminders.push({
|
||||||
time: null,
|
|
||||||
value: data.value,
|
value: data.value,
|
||||||
unit: data.unit,
|
unit: data.unit,
|
||||||
label: AddonCalendar.getUnitValueLabel(data.value, data.unit, true),
|
label: CoreReminders.getUnitValueLabel(data.value, data.unit, true),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -670,8 +670,15 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
|
||||||
* Add a reminder.
|
* Add a reminder.
|
||||||
*/
|
*/
|
||||||
async addReminder(): Promise<void> {
|
async addReminder(): Promise<void> {
|
||||||
const reminderTime = await CoreDomUtils.openModal<number>({
|
const formData = this.form.value;
|
||||||
component: AddonCalendarReminderTimeModalComponent,
|
const eventTime = CoreTimeUtils.convertToTimestamp(formData.timestart, true);
|
||||||
|
|
||||||
|
const reminderTime = await CoreDomUtils.openPopover<{timeBefore: number}>({
|
||||||
|
component: CoreRemindersSetReminderMenuComponent,
|
||||||
|
componentProps: {
|
||||||
|
eventTime,
|
||||||
|
},
|
||||||
|
// TODO: Add event to open the popover in place.
|
||||||
});
|
});
|
||||||
|
|
||||||
if (reminderTime === undefined) {
|
if (reminderTime === undefined) {
|
||||||
|
@ -679,14 +686,14 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = AddonCalendarProvider.convertSecondsToValueAndUnit(reminderTime);
|
const data = CoreRemindersService.convertSecondsToValueAndUnit(reminderTime.timeBefore);
|
||||||
|
|
||||||
// Add reminder.
|
// Add reminder.
|
||||||
this.reminders.push({
|
this.reminders.push({
|
||||||
time: reminderTime,
|
time: reminderTime.timeBefore,
|
||||||
value: data.value,
|
value: data.value,
|
||||||
unit: data.unit,
|
unit: data.unit,
|
||||||
label: AddonCalendar.getUnitValueLabel(data.value, data.unit),
|
label: CoreReminders.getUnitValueLabel(data.value, data.unit),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -697,13 +704,13 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
|
||||||
*/
|
*/
|
||||||
removeReminder(reminder: AddonCalendarEventCandidateReminder): void {
|
removeReminder(reminder: AddonCalendarEventCandidateReminder): void {
|
||||||
const index = this.reminders.indexOf(reminder);
|
const index = this.reminders.indexOf(reminder);
|
||||||
if (index != -1) {
|
if (index !== -1) {
|
||||||
this.reminders.splice(index, 1);
|
this.reminders.splice(index, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page destroyed.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.unblockSync();
|
this.unblockSync();
|
||||||
|
@ -712,4 +719,9 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddonCalendarEventCandidateReminder = Omit<AddonCalendarEventReminder, 'id'|'eventid'>;
|
type AddonCalendarEventCandidateReminder = {
|
||||||
|
time?: number; // Undefined for default reminder.
|
||||||
|
value: number; // Amount of time.
|
||||||
|
unit: CoreRemindersUnits; // Units.
|
||||||
|
label: string; // Label to represent the reminder.
|
||||||
|
};
|
||||||
|
|
|
@ -127,7 +127,7 @@
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
|
||||||
<ion-card *ngIf="notificationsEnabled && event">
|
<ion-card *ngIf="remindersEnabled && event">
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{ 'addon.calendar.reminders' | translate }}</h2>
|
<h2>{{ 'addon.calendar.reminders' | translate }}</h2>
|
||||||
|
@ -139,7 +139,7 @@
|
||||||
<p class="item-heading">{{ reminder.label }}</p>
|
<p class="item-heading">{{ reminder.label }}</p>
|
||||||
<p *ngIf="reminder.sublabel">{{ reminder.sublabel }}</p>
|
<p *ngIf="reminder.sublabel">{{ reminder.sublabel }}</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-button fill="clear" (click)="cancelNotification(reminder.id, $event)" [attr.aria-label]="'core.delete' | translate"
|
<ion-button fill="clear" (click)="deleteReminder(reminder.id, $event)" [attr.aria-label]="'core.delete' | translate"
|
||||||
slot="end">
|
slot="end">
|
||||||
<ion-icon name="fas-trash" color="danger" slot="icon-only" aria-hidden="true"></ion-icon>
|
<ion-icon name="fas-trash" color="danger" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|
|
@ -28,7 +28,6 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { CoreDomUtils, ToastDuration } from '@services/utils/dom';
|
import { CoreDomUtils, ToastDuration } from '@services/utils/dom';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreLocalNotifications } from '@services/local-notifications';
|
|
||||||
import { CoreCourse } from '@features/course/services/course';
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
import { CoreTimeUtils } from '@services/utils/time';
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
import { CoreGroups } from '@services/groups';
|
import { CoreGroups } from '@services/groups';
|
||||||
|
@ -38,10 +37,11 @@ import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { CoreConstants } from '@/core/constants';
|
import { CoreConstants } from '@/core/constants';
|
||||||
import { AddonCalendarReminderTimeModalComponent } from '@addons/calendar/components/reminder-time-modal/reminder-time-modal';
|
|
||||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
import { AddonCalendarEventsSource } from '@addons/calendar/classes/events-source';
|
import { AddonCalendarEventsSource } from '@addons/calendar/classes/events-source';
|
||||||
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
|
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
|
||||||
|
import { CoreReminders, CoreRemindersService } from '@features/reminders/services/reminders';
|
||||||
|
import { CoreRemindersSetReminderMenuComponent } from '@features/reminders/components/set-reminder-menu/set-reminder-menu';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays a single calendar event.
|
* Page that displays a single calendar event.
|
||||||
|
@ -71,7 +71,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
|
||||||
courseName = '';
|
courseName = '';
|
||||||
groupName?: string;
|
groupName?: string;
|
||||||
courseUrl = '';
|
courseUrl = '';
|
||||||
notificationsEnabled = false;
|
remindersEnabled = false;
|
||||||
moduleUrl = '';
|
moduleUrl = '';
|
||||||
categoryPath = '';
|
categoryPath = '';
|
||||||
currentTime = -1;
|
currentTime = -1;
|
||||||
|
@ -84,7 +84,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
|
||||||
constructor(
|
constructor(
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
) {
|
) {
|
||||||
this.notificationsEnabled = CoreLocalNotifications.isAvailable();
|
this.remindersEnabled = CoreReminders.isEnabled();
|
||||||
this.siteHomeId = CoreSites.getCurrentSiteHomeId();
|
this.siteHomeId = CoreSites.getCurrentSiteHomeId();
|
||||||
this.currentSiteId = CoreSites.getCurrentSiteId();
|
this.currentSiteId = CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
|
@ -131,7 +131,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reload reminders if default notification time changes.
|
// Reload reminders if default notification time changes.
|
||||||
this.defaultTimeChangedObserver = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
|
this.defaultTimeChangedObserver = CoreEvents.on(CoreRemindersService.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
|
||||||
this.loadReminders();
|
this.loadReminders();
|
||||||
}, this.currentSiteId);
|
}, this.currentSiteId);
|
||||||
|
|
||||||
|
@ -148,16 +148,15 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async loadReminders(): Promise<void> {
|
protected async loadReminders(): Promise<void> {
|
||||||
if (!this.notificationsEnabled || !this.event) {
|
if (!this.remindersEnabled || !this.event) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reminders = await AddonCalendar.getEventReminders(this.eventId, this.currentSiteId);
|
this.reminders = await AddonCalendarHelper.getEventReminders(this.eventId, this.event.timestart, this.currentSiteId);
|
||||||
this.reminders = await AddonCalendarHelper.formatReminders(reminders, this.event.timestart, this.currentSiteId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View loaded.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
@ -387,8 +386,12 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reminderTime = await CoreDomUtils.openModal<number>({
|
const reminderTime = await CoreDomUtils.openPopover<{timeBefore: number}>({
|
||||||
component: AddonCalendarReminderTimeModalComponent,
|
component: CoreRemindersSetReminderMenuComponent,
|
||||||
|
componentProps: {
|
||||||
|
eventTime: this.event.timestart,
|
||||||
|
},
|
||||||
|
// TODO: Add event to open the popover in place.
|
||||||
});
|
});
|
||||||
|
|
||||||
if (reminderTime === undefined) {
|
if (reminderTime === undefined) {
|
||||||
|
@ -396,18 +399,18 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await AddonCalendar.addEventReminder(this.event, reminderTime, this.currentSiteId);
|
await AddonCalendar.addEventReminder(this.event, reminderTime.timeBefore, this.currentSiteId);
|
||||||
|
|
||||||
await this.loadReminders();
|
await this.loadReminders();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancel the selected notification.
|
* Delete the selected reminder.
|
||||||
*
|
*
|
||||||
* @param id Reminder ID.
|
* @param id Reminder ID.
|
||||||
* @param e Click event.
|
* @param e Click event.
|
||||||
*/
|
*/
|
||||||
async cancelNotification(id: number, e: Event): Promise<void> {
|
async deleteReminder(id: number, e: Event): Promise<void> {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
@ -417,7 +420,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
|
||||||
const modal = await CoreDomUtils.showModalLoading('core.deleting', true);
|
const modal = await CoreDomUtils.showModalLoading('core.deleting', true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await AddonCalendar.deleteEventReminder(id);
|
await CoreReminders.removeReminder(id);
|
||||||
await this.loadReminders();
|
await this.loadReminders();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'Error deleting reminder');
|
CoreDomUtils.showErrorModalDefault(error, 'Error deleting reminder');
|
||||||
|
@ -636,7 +639,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page destroyed.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.editEventObserver.off();
|
this.editEventObserver.off();
|
||||||
|
|
|
@ -15,8 +15,8 @@
|
||||||
iconAction="fas-th-list" (action)="toggleDisplay()"></core-context-menu-item>
|
iconAction="fas-th-list" (action)="toggleDisplay()"></core-context-menu-item>
|
||||||
<core-context-menu-item *ngIf="!showCalendar" [priority]="800" [content]="'addon.calendar.monthlyview' | translate"
|
<core-context-menu-item *ngIf="!showCalendar" [priority]="800" [content]="'addon.calendar.monthlyview' | translate"
|
||||||
iconAction="fas-calendar-alt" (action)="toggleDisplay()"></core-context-menu-item>
|
iconAction="fas-calendar-alt" (action)="toggleDisplay()"></core-context-menu-item>
|
||||||
<core-context-menu-item [hidden]="!notificationsEnabled" [priority]="600" [content]="'core.settings.settings' | translate"
|
<core-context-menu-item [priority]="600" [content]="'core.settings.settings' | translate" (action)="openSettings()"
|
||||||
(action)="openSettings()" iconAction="fas-cogs">
|
iconAction="fas-cogs">
|
||||||
</core-context-menu-item>
|
</core-context-menu-item>
|
||||||
<core-context-menu-item [hidden]="!loaded || !hasOffline || !isOnline" [priority]="400"
|
<core-context-menu-item [hidden]="!loaded || !hasOffline || !isOnline" [priority]="400"
|
||||||
[content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event, true)"
|
[content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event, true)"
|
||||||
|
|
|
@ -31,7 +31,6 @@ import { AddonCalendarCalendarComponent } from '../../components/calendar/calend
|
||||||
import { AddonCalendarUpcomingEventsComponent } from '../../components/upcoming-events/upcoming-events';
|
import { AddonCalendarUpcomingEventsComponent } from '../../components/upcoming-events/upcoming-events';
|
||||||
import { AddonCalendarFilterComponent } from '../../components/filter/filter';
|
import { AddonCalendarFilterComponent } from '../../components/filter/filter';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreLocalNotifications } from '@services/local-notifications';
|
|
||||||
import { CoreConstants } from '@/core/constants';
|
import { CoreConstants } from '@/core/constants';
|
||||||
import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager';
|
import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager';
|
||||||
|
|
||||||
|
@ -64,7 +63,6 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
|
||||||
month?: number;
|
month?: number;
|
||||||
canCreate = false;
|
canCreate = false;
|
||||||
courses: Partial<CoreEnrolledCourseData>[] = [];
|
courses: Partial<CoreEnrolledCourseData>[] = [];
|
||||||
notificationsEnabled = false;
|
|
||||||
loaded = false;
|
loaded = false;
|
||||||
hasOffline = false;
|
hasOffline = false;
|
||||||
isOnline = false;
|
isOnline = false;
|
||||||
|
@ -165,8 +163,6 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
|
||||||
* View loaded.
|
* View loaded.
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.notificationsEnabled = CoreLocalNotifications.isAvailable();
|
|
||||||
|
|
||||||
this.loadUpcoming = !!CoreNavigator.getRouteBooleanParam('upcoming');
|
this.loadUpcoming = !!CoreNavigator.getRouteBooleanParam('upcoming');
|
||||||
this.showCalendar = !this.loadUpcoming;
|
this.showCalendar = !this.loadUpcoming;
|
||||||
|
|
||||||
|
|
|
@ -13,16 +13,12 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
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 { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { AddonCalendarReminderTimeModalComponent } from '@addons/calendar/components/reminder-time-modal/reminder-time-modal';
|
import {
|
||||||
|
CoreReminders,
|
||||||
|
CoreRemindersService,
|
||||||
|
} from '@features/reminders/services/reminders';
|
||||||
|
import { CoreRemindersSetReminderMenuComponent } from '@features/reminders/components/set-reminder-menu/set-reminder-menu';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays the calendar settings.
|
* Page that displays the calendar settings.
|
||||||
|
@ -35,19 +31,13 @@ export class AddonCalendarSettingsPage implements OnInit {
|
||||||
|
|
||||||
defaultTimeLabel = '';
|
defaultTimeLabel = '';
|
||||||
|
|
||||||
protected defaultTime: AddonCalendarValueAndUnit = {
|
protected defaultTime?: number;
|
||||||
value: 0,
|
|
||||||
unit: AddonCalendarReminderUnits.MINUTE,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View loaded.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
const defaultTime = await AddonCalendar.getDefaultNotificationTime();
|
this.updateDefaultTimeLabel();
|
||||||
|
|
||||||
this.defaultTime = AddonCalendarProvider.convertSecondsToValueAndUnit(defaultTime);
|
|
||||||
this.defaultTimeLabel = AddonCalendar.getUnitValueLabel(this.defaultTime.value, this.defaultTime.unit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,12 +51,13 @@ export class AddonCalendarSettingsPage implements OnInit {
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const reminderTime = await CoreDomUtils.openModal<number>({
|
const reminderTime = await CoreDomUtils.openPopover<{timeBefore: number}>({
|
||||||
component: AddonCalendarReminderTimeModalComponent,
|
component: CoreRemindersSetReminderMenuComponent,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
initialValue: this.defaultTime,
|
initialValue: this.defaultTime,
|
||||||
allowDisable: true,
|
noReminderLabel: 'core.settings.disabled',
|
||||||
},
|
},
|
||||||
|
event: e,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (reminderTime === undefined) {
|
if (reminderTime === undefined) {
|
||||||
|
@ -74,25 +65,18 @@ export class AddonCalendarSettingsPage implements OnInit {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.defaultTime = AddonCalendarProvider.convertSecondsToValueAndUnit(reminderTime);
|
await CoreReminders.setDefaultNotificationTime(reminderTime.timeBefore ?? CoreRemindersService.DISABLED);
|
||||||
this.defaultTimeLabel = AddonCalendar.getUnitValueLabel(this.defaultTime.value, this.defaultTime.unit);
|
this.updateDefaultTimeLabel();
|
||||||
|
|
||||||
this.updateDefaultTime(reminderTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update default time.
|
* Update default time label.
|
||||||
*
|
|
||||||
* @param newTime New time.
|
|
||||||
*/
|
*/
|
||||||
updateDefaultTime(newTime: number): void {
|
async updateDefaultTimeLabel(): Promise<void> {
|
||||||
AddonCalendar.setDefaultNotificationTime(newTime);
|
this.defaultTime = await CoreReminders.getDefaultNotificationTime();
|
||||||
|
|
||||||
CoreEvents.trigger(
|
const defaultTime = CoreRemindersService.convertSecondsToValueAndUnit(this.defaultTime);
|
||||||
AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED,
|
this.defaultTimeLabel = CoreReminders.getUnitValueLabel(defaultTime.value, defaultTime.unit);
|
||||||
{ time: newTime },
|
|
||||||
CoreSites.getCurrentSiteId(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@ import {
|
||||||
AddonCalendarEventType,
|
AddonCalendarEventType,
|
||||||
AddonCalendarGetEventsEvent,
|
AddonCalendarGetEventsEvent,
|
||||||
AddonCalendarProvider,
|
AddonCalendarProvider,
|
||||||
AddonCalendarReminderUnits,
|
|
||||||
AddonCalendarWeek,
|
AddonCalendarWeek,
|
||||||
AddonCalendarWeekDay,
|
AddonCalendarWeekDay,
|
||||||
} from './calendar';
|
} from './calendar';
|
||||||
|
@ -36,8 +35,8 @@ import { makeSingleton } from '@singletons';
|
||||||
import { AddonCalendarSyncInvalidateEvent } from './calendar-sync';
|
import { AddonCalendarSyncInvalidateEvent } from './calendar-sync';
|
||||||
import { AddonCalendarOfflineEventDBRecord } from './database/calendar-offline';
|
import { AddonCalendarOfflineEventDBRecord } from './database/calendar-offline';
|
||||||
import { CoreCategoryData } from '@features/courses/services/courses';
|
import { CoreCategoryData } from '@features/courses/services/courses';
|
||||||
import { AddonCalendarReminderDBRecord } from './database/calendar';
|
|
||||||
import { CoreTimeUtils } from '@services/utils/time';
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
|
import { CoreReminders, CoreRemindersService } from '@features/reminders/services/reminders';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context levels enumeration.
|
* Context levels enumeration.
|
||||||
|
@ -123,7 +122,7 @@ export class AddonCalendarHelperProvider {
|
||||||
* Classify events into their respective months and days. If an event duration covers more than one day,
|
* Classify events into their respective months and days. If an event duration covers more than one day,
|
||||||
* it will be included in all the days it lasts.
|
* it will be included in all the days it lasts.
|
||||||
*
|
*
|
||||||
* @param events Events to classify.
|
* @param offlineEvents Events to classify.
|
||||||
* @return Object with the classified events.
|
* @return Object with the classified events.
|
||||||
*/
|
*/
|
||||||
classifyIntoMonths(
|
classifyIntoMonths(
|
||||||
|
@ -219,7 +218,7 @@ export class AddonCalendarHelperProvider {
|
||||||
/**
|
/**
|
||||||
* Convenience function to format some event data to be rendered.
|
* Convenience function to format some event data to be rendered.
|
||||||
*
|
*
|
||||||
* @param e Event to format.
|
* @param event Event to format.
|
||||||
*/
|
*/
|
||||||
formatOfflineEventData(event: AddonCalendarOfflineEventDBRecord): AddonCalendarEventToDisplay {
|
formatOfflineEventData(event: AddonCalendarOfflineEventDBRecord): AddonCalendarEventToDisplay {
|
||||||
|
|
||||||
|
@ -295,48 +294,75 @@ export class AddonCalendarHelperProvider {
|
||||||
* @param timestart Event timestart.
|
* @param timestart Event timestart.
|
||||||
* @param siteId Site ID.
|
* @param siteId Site ID.
|
||||||
* @return Formatted reminders.
|
* @return Formatted reminders.
|
||||||
|
* @deprecated since 4.1 Use AddonCalendarHelper.getEventReminders.
|
||||||
*/
|
*/
|
||||||
async formatReminders(
|
async formatReminders(
|
||||||
reminders: AddonCalendarReminderDBRecord[],
|
reminders: { eventid: number }[],
|
||||||
timestart: number,
|
timestart: number,
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<AddonCalendarEventReminder[]> {
|
): Promise<AddonCalendarEventReminder[]> {
|
||||||
const defaultTime = await AddonCalendar.getDefaultNotificationTime(siteId);
|
if (!reminders.length) {
|
||||||
|
return [];
|
||||||
const formattedReminders = <AddonCalendarEventReminder[]> 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) => {
|
return AddonCalendarHelper.getEventReminders(reminders[0].eventid, timestart, siteId);
|
||||||
if (reminder.time === null) {
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format reminders, adding calculated data.
|
||||||
|
*
|
||||||
|
* @param eventId Event Id.
|
||||||
|
* @param eventTimestart Event timestart.
|
||||||
|
* @param siteId Site ID.
|
||||||
|
* @return Formatted reminders.
|
||||||
|
*/
|
||||||
|
async getEventReminders(
|
||||||
|
eventId: number,
|
||||||
|
eventTimestart: number,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<AddonCalendarEventReminder[]> {
|
||||||
|
const reminders = await CoreReminders.getReminders(
|
||||||
|
{
|
||||||
|
instanceId: eventId,
|
||||||
|
component: AddonCalendarProvider.COMPONENT,
|
||||||
|
},
|
||||||
|
siteId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!reminders.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTime = await CoreReminders.getDefaultNotificationTime(siteId);
|
||||||
|
let defaultLabel: string | undefined;
|
||||||
|
|
||||||
|
if (defaultTime > CoreRemindersService.DISABLED) {
|
||||||
|
const data = CoreRemindersService.convertSecondsToValueAndUnit(defaultTime);
|
||||||
|
defaultLabel = CoreReminders.getUnitValueLabel(data.value, data.unit, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reminders.map((reminder) => {
|
||||||
|
const formatted: AddonCalendarEventReminder = {
|
||||||
|
id: reminder.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (reminder.timebefore === CoreRemindersService.DEFAULT_REMINDER_TIMEBEFORE) {
|
||||||
// Default time. Check if default notifications are disabled.
|
// Default time. Check if default notifications are disabled.
|
||||||
if (defaultTimeValue !== undefined && defaultTimeUnit) {
|
if (defaultLabel !== undefined) {
|
||||||
reminder.value = defaultTimeValue;
|
formatted.label = defaultLabel;
|
||||||
reminder.unit = defaultTimeUnit;
|
formatted.timestamp = eventTimestart - defaultTime;
|
||||||
reminder.timestamp = eventTimestart - reminder.value * reminder.unit;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const data = AddonCalendarProvider.convertSecondsToValueAndUnit(reminder.time);
|
const data = CoreRemindersService.convertSecondsToValueAndUnit(reminder.timebefore);
|
||||||
reminder.value = data.value;
|
formatted.label = CoreReminders.getUnitValueLabel(data.value, data.unit, false);
|
||||||
reminder.unit = data.unit;
|
formatted.timestamp = eventTimestart - reminder.timebefore;
|
||||||
reminder.timestamp = eventTimestart - reminder.time;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reminder.value && reminder.unit) {
|
if (formatted.timestamp) {
|
||||||
reminder.label = AddonCalendar.getUnitValueLabel(reminder.value, reminder.unit, reminder.time === null);
|
formatted.sublabel = CoreTimeUtils.userDate(formatted.timestamp * 1000, 'core.strftimedatetime');
|
||||||
if (reminder.timestamp) {
|
|
||||||
reminder.sublabel = CoreTimeUtils.userDate(reminder.timestamp * 1000, 'core.strftimedatetime');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return reminder;
|
return formatted;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -381,7 +407,7 @@ export class AddonCalendarHelperProvider {
|
||||||
/**
|
/**
|
||||||
* Get the day "id".
|
* Get the day "id".
|
||||||
*
|
*
|
||||||
* @param day Day moment.
|
* @param moment Day moment.
|
||||||
* @return The "id".
|
* @return The "id".
|
||||||
*/
|
*/
|
||||||
getDayId(moment: moment.Moment): string {
|
getDayId(moment: moment.Moment): string {
|
||||||
|
@ -553,18 +579,18 @@ export class AddonCalendarHelperProvider {
|
||||||
courseId: number,
|
courseId: number,
|
||||||
categoryId?: number,
|
categoryId?: number,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (event.eventtype == 'user' || event.eventtype == 'site') {
|
if (event.eventtype === 'user' || event.eventtype === 'site') {
|
||||||
// User or site event, display it.
|
// User or site event, display it.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.eventtype == 'category' && categories) {
|
if (event.eventtype === 'category' && categories) {
|
||||||
if (!event.categoryid || !Object.keys(categories).length || !categoryId) {
|
if (!event.categoryid || !Object.keys(categories).length || !categoryId) {
|
||||||
// We can't tell if the course belongs to the category, display them all.
|
// We can't tell if the course belongs to the category, display them all.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.categoryid == categoryId) {
|
if (event.categoryid === categoryId) {
|
||||||
// The event is in the same category as the course, display it.
|
// The event is in the same category as the course, display it.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -577,7 +603,7 @@ export class AddonCalendarHelperProvider {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.categoryid == category.parent) {
|
if (event.categoryid === category.parent) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
category = categories[category.parent];
|
category = categories[category.parent];
|
||||||
|
@ -796,9 +822,8 @@ export type AddonCalendarEventTypeOption = {
|
||||||
/**
|
/**
|
||||||
* Formatted event reminder.
|
* Formatted event reminder.
|
||||||
*/
|
*/
|
||||||
export type AddonCalendarEventReminder = AddonCalendarReminderDBRecord & {
|
export type AddonCalendarEventReminder = {
|
||||||
value?: number; // Amount of time.
|
id: number;
|
||||||
unit?: AddonCalendarReminderUnits; // Units.
|
|
||||||
timestamp?: number; // Timestamp (in seconds).
|
timestamp?: number; // Timestamp (in seconds).
|
||||||
label?: string; // Label to represent the reminder.
|
label?: string; // Label to represent the reminder.
|
||||||
sublabel?: string; // Sub label.
|
sublabel?: string; // Sub label.
|
||||||
|
|
|
@ -23,12 +23,11 @@ import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreGroups } from '@services/groups';
|
import { CoreGroups } from '@services/groups';
|
||||||
import { CoreLocalNotifications } from '@services/local-notifications';
|
import { CoreLocalNotifications } from '@services/local-notifications';
|
||||||
import { CoreConfig } from '@services/config';
|
import { CoreConfig } from '@services/config';
|
||||||
import { ILocalNotification } from '@ionic-native/local-notifications';
|
|
||||||
import { AddonCalendarOffline } from './calendar-offline';
|
import { AddonCalendarOffline } from './calendar-offline';
|
||||||
import { CoreUser } from '@features/user/services/user';
|
import { CoreUser } from '@features/user/services/user';
|
||||||
import { CoreWSExternalWarning, CoreWSDate } from '@services/ws';
|
import { CoreWSExternalWarning, CoreWSDate } from '@services/ws';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import { AddonCalendarEventDBRecord, AddonCalendarReminderDBRecord, EVENTS_TABLE, REMINDERS_TABLE } from './database/calendar';
|
import { AddonCalendarEventDBRecord, EVENTS_TABLE } from './database/calendar';
|
||||||
import { CoreCourses } from '@features/courses/services/courses';
|
import { CoreCourses } from '@features/courses/services/courses';
|
||||||
import { ContextLevel, CoreConstants } from '@/core/constants';
|
import { ContextLevel, CoreConstants } from '@/core/constants';
|
||||||
import { CoreWSError } from '@classes/errors/wserror';
|
import { CoreWSError } from '@classes/errors/wserror';
|
||||||
|
@ -39,9 +38,17 @@ import { SafeUrl } from '@angular/platform-browser';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { AddonCalendarFilter } from './calendar-helper';
|
import { AddonCalendarFilter } from './calendar-helper';
|
||||||
import { AddonCalendarSyncEvents, AddonCalendarSyncProvider } from './calendar-sync';
|
import { AddonCalendarSyncEvents, AddonCalendarSyncProvider } from './calendar-sync';
|
||||||
import { CoreEvents } from '@singletons/events';
|
|
||||||
import { CoreText } from '@singletons/text';
|
import { CoreText } from '@singletons/text';
|
||||||
import { CorePlatform } from '@services/platform';
|
import { CorePlatform } from '@services/platform';
|
||||||
|
import {
|
||||||
|
CoreReminderData,
|
||||||
|
CoreReminders,
|
||||||
|
CoreRemindersPushNotificationData,
|
||||||
|
CoreRemindersService,
|
||||||
|
CoreRemindersUnits,
|
||||||
|
CoreReminderValueAndUnit,
|
||||||
|
} from '@features/reminders/services/reminders';
|
||||||
|
import { CoreReminderDBRecord } from '@features/reminders/services/database/reminders';
|
||||||
|
|
||||||
const ROOT_CACHE_KEY = 'mmaCalendar:';
|
const ROOT_CACHE_KEY = 'mmaCalendar:';
|
||||||
|
|
||||||
|
@ -58,6 +65,8 @@ export enum AddonCalendarEventType {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Units to set a reminder.
|
* Units to set a reminder.
|
||||||
|
*
|
||||||
|
* @deprecated since 4.1 Use CoreReminderUnits instead.
|
||||||
*/
|
*/
|
||||||
export enum AddonCalendarReminderUnits {
|
export enum AddonCalendarReminderUnits {
|
||||||
MINUTE = CoreConstants.SECONDS_MINUTE,
|
MINUTE = CoreConstants.SECONDS_MINUTE,
|
||||||
|
@ -85,21 +94,6 @@ 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.
|
* Service to handle calendar events.
|
||||||
*/
|
*/
|
||||||
|
@ -108,8 +102,7 @@ export class AddonCalendarProvider {
|
||||||
|
|
||||||
static readonly DAYS_INTERVAL = 30;
|
static readonly DAYS_INTERVAL = 30;
|
||||||
static readonly COMPONENT = 'AddonCalendarEvents';
|
static readonly COMPONENT = 'AddonCalendarEvents';
|
||||||
static readonly DEFAULT_NOTIFICATION_TIME_CHANGED = 'AddonCalendarDefaultNotificationTimeChangedEvent';
|
|
||||||
static readonly DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime';
|
|
||||||
static readonly STARTING_WEEK_DAY = 'addon_calendar_starting_week_day';
|
static readonly STARTING_WEEK_DAY = 'addon_calendar_starting_week_day';
|
||||||
static readonly NEW_EVENT_EVENT = 'addon_calendar_new_event';
|
static readonly NEW_EVENT_EVENT = 'addon_calendar_new_event';
|
||||||
static readonly NEW_EVENT_DISCARDED_EVENT = 'addon_calendar_new_event_discarded';
|
static readonly NEW_EVENT_DISCARDED_EVENT = 'addon_calendar_new_event_discarded';
|
||||||
|
@ -188,34 +181,10 @@ export class AddonCalendarProvider {
|
||||||
*
|
*
|
||||||
* @param seconds Number of seconds.
|
* @param seconds Number of seconds.
|
||||||
* @return Value and unit.
|
* @return Value and unit.
|
||||||
|
* @deprecated since 4.1 Use CoreRemindersService.convertSecondsToValueAndUnit instead.
|
||||||
*/
|
*/
|
||||||
static convertSecondsToValueAndUnit(seconds: number): AddonCalendarValueAndUnit {
|
static convertSecondsToValueAndUnit(seconds: number): CoreReminderValueAndUnit {
|
||||||
if (seconds <= 0) {
|
return CoreRemindersService.convertSecondsToValueAndUnit(seconds);
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -306,17 +275,12 @@ export class AddonCalendarProvider {
|
||||||
EVENTS_TABLE,
|
EVENTS_TABLE,
|
||||||
{ id: eventId },
|
{ id: eventId },
|
||||||
));
|
));
|
||||||
promises.push(site.getDb().getRecords<AddonCalendarReminderDBRecord>(
|
promises.push(CoreReminders.removeReminders({
|
||||||
REMINDERS_TABLE,
|
instanceId: eventId,
|
||||||
{ eventid: eventId },
|
component: AddonCalendarProvider.COMPONENT,
|
||||||
).then((reminders) =>
|
} , siteId));
|
||||||
Promise.all(reminders.map((reminder) => this.deleteEventReminder(reminder.id, siteId)))));
|
|
||||||
|
|
||||||
try {
|
await CoreUtils.ignoreErrors(Promise.all(promises));
|
||||||
await Promise.all(promises);
|
|
||||||
} catch {
|
|
||||||
// Ignore errors.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -325,56 +289,40 @@ export class AddonCalendarProvider {
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
|
CoreLocalNotifications.registerClick<CoreRemindersPushNotificationData>(
|
||||||
CoreLocalNotifications.registerClick<AddonCalendarPushNotificationData>(
|
|
||||||
AddonCalendarProvider.COMPONENT,
|
AddonCalendarProvider.COMPONENT,
|
||||||
async (notification) => {
|
async (notification) => {
|
||||||
if (notification.eventId) {
|
await ApplicationInit.donePromise;
|
||||||
await ApplicationInit.donePromise;
|
|
||||||
|
|
||||||
const disabled = await this.isDisabled(notification.siteId);
|
this.notificationClicked(notification);
|
||||||
if (disabled) {
|
|
||||||
// The calendar is disabled in the site, don't open it.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CoreNavigator.navigateToSitePath(
|
|
||||||
AddonCalendarMainMenuHandlerService.PAGE_NAME,
|
|
||||||
{
|
|
||||||
siteId: notification.siteId,
|
|
||||||
preferCurrentTab: false,
|
|
||||||
nextNavigation: {
|
|
||||||
path: `calendar/event/${notification.eventId}`,
|
|
||||||
isSitePath: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (CoreLocalNotifications.isAvailable()) {
|
/**
|
||||||
CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, async (data) => {
|
* Notification has been clicked.
|
||||||
const site = await CoreSites.getSite(data.siteId);
|
*
|
||||||
|
* @param notification Calendar notification.
|
||||||
// Get all the events that have a default reminder.
|
* @return Promise resolved when done.
|
||||||
const query = 'SELECT events.*, reminders.id AS reminderid ' +
|
*/
|
||||||
'FROM ' + EVENTS_TABLE + ' events ' +
|
async notificationClicked(notification: CoreRemindersPushNotificationData): Promise<void> {
|
||||||
'INNER JOIN ' + REMINDERS_TABLE + ' reminders ON events.id = reminders.eventid ' +
|
const disabled = await this.isDisabled(notification.siteId);
|
||||||
'WHERE reminders.time IS NULL';
|
if (disabled) {
|
||||||
|
// The calendar is disabled in the site, don't open it.
|
||||||
const result = await site.getDb().execute(query);
|
return;
|
||||||
|
|
||||||
// Reschedule all the default reminders.
|
|
||||||
for (let i = 0; i < result.rows.length; i++) {
|
|
||||||
const event = result.rows.item(i) as AddonCalendarEventDBRecord & {
|
|
||||||
reminderid: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.scheduleEventNotification(event, event.reminderid, null, site.getId());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CoreNavigator.navigateToSitePath(
|
||||||
|
AddonCalendarMainMenuHandlerService.PAGE_NAME,
|
||||||
|
{
|
||||||
|
siteId: notification.siteId,
|
||||||
|
preferCurrentTab: false,
|
||||||
|
nextNavigation: {
|
||||||
|
path: `calendar/event/${notification.instanceId}`,
|
||||||
|
isSitePath: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -527,7 +475,7 @@ export class AddonCalendarProvider {
|
||||||
await site.read('core_calendar_get_allowed_event_types', params, preSets);
|
await site.read('core_calendar_get_allowed_event_types', params, preSets);
|
||||||
|
|
||||||
// Convert the array to an object.
|
// Convert the array to an object.
|
||||||
const result = {};
|
const result: {[name: string]: boolean} = {};
|
||||||
if (response.allowedeventtypes) {
|
if (response.allowedeventtypes) {
|
||||||
response.allowedeventtypes.forEach((type) => {
|
response.allowedeventtypes.forEach((type) => {
|
||||||
result[type] = true;
|
result[type] = true;
|
||||||
|
@ -633,13 +581,10 @@ export class AddonCalendarProvider {
|
||||||
*
|
*
|
||||||
* @param siteId ID of the site. If not defined, use current site.
|
* @param siteId ID of the site. If not defined, use current site.
|
||||||
* @return Promise resolved with the default time (in seconds).
|
* @return Promise resolved with the default time (in seconds).
|
||||||
|
* @deprecated since 4.1 Use CoreReminders.getDefaultNotificationTime instead.
|
||||||
*/
|
*/
|
||||||
async getDefaultNotificationTime(siteId?: string): Promise<number> {
|
async getDefaultNotificationTime(siteId?: string): Promise<number> {
|
||||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
return CoreReminders.getDefaultNotificationTime(siteId);
|
||||||
|
|
||||||
const key = AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId;
|
|
||||||
|
|
||||||
return CoreConfig.get(key, CoreConstants.CONFIG.calendarreminderdefaultvalue || 3600);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -699,7 +644,7 @@ export class AddonCalendarProvider {
|
||||||
await site.read('core_calendar_get_calendar_event_by_id', params, preSets);
|
await site.read('core_calendar_get_calendar_event_by_id', params, preSets);
|
||||||
|
|
||||||
this.storeEventInLocalDb(response.event, { siteId });
|
this.storeEventInLocalDb(response.event, { siteId });
|
||||||
this.scheduleEventsNotifications([response.event], siteId);
|
this.updateEventsReminders([response.event], site.getId());
|
||||||
|
|
||||||
return response.event;
|
return response.event;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -766,27 +711,43 @@ export class AddonCalendarProvider {
|
||||||
* Adds an event reminder and schedule a new notification.
|
* Adds an event reminder and schedule a new notification.
|
||||||
*
|
*
|
||||||
* @param event Event to set the reminder.
|
* @param event Event to set the reminder.
|
||||||
* @param time Amount of seconds of the reminder. Undefined for default reminder.
|
* @param timebefore Amount of seconds of the reminder. Undefined for default reminder.
|
||||||
* @param siteId ID of the site the event belongs to. If not defined, use current site.
|
* @param siteId ID of the site the event belongs to. If not defined, use current site.
|
||||||
* @return Promise resolved when the notification is updated.
|
* @return Promise resolved when the notification is updated.
|
||||||
*/
|
*/
|
||||||
async addEventReminder(
|
async addEventReminder(
|
||||||
event: { id: number; timestart: number; name: string},
|
event: AddonCalendarEvent | AddonCalendarEventDBRecord | AddonCalendarEventToDisplay | AddonCalendarOfflineEventDBRecord,
|
||||||
time?: number | null,
|
timebefore?: number,
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
|
||||||
const reminder: Partial<AddonCalendarReminderDBRecord> = {
|
timebefore = timebefore ?? CoreRemindersService.DEFAULT_REMINDER_TIMEBEFORE;
|
||||||
eventid: event.id,
|
|
||||||
time: time ?? null,
|
const previousReminders = await CoreReminders.getReminders({
|
||||||
timecreated: Date.now(),
|
instanceId: event.id,
|
||||||
|
component: AddonCalendarProvider.COMPONENT,
|
||||||
|
}, siteId);
|
||||||
|
|
||||||
|
if (previousReminders.some((reminder) => reminder.timebefore === timebefore)) {
|
||||||
|
// Already exists.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = 'url' in event
|
||||||
|
? event.url || ''
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const reminder: CoreReminderData = {
|
||||||
|
component: AddonCalendarProvider.COMPONENT,
|
||||||
|
instanceId: event.id,
|
||||||
|
type: event.eventtype,
|
||||||
|
time: event.timestart,
|
||||||
|
timebefore,
|
||||||
|
title: event.name,
|
||||||
|
url,
|
||||||
};
|
};
|
||||||
|
|
||||||
const reminderId = await site.getDb().insertRecord(REMINDERS_TABLE, reminder);
|
await CoreReminders.addReminder(reminder, siteId);
|
||||||
|
|
||||||
const timestamp = time ? event.timestart - time : time;
|
|
||||||
|
|
||||||
await this.scheduleEventNotification(event, reminderId, timestamp, site.getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -810,15 +771,10 @@ export class AddonCalendarProvider {
|
||||||
* @param id Reminder ID.
|
* @param id Reminder ID.
|
||||||
* @param siteId ID of the site the event belongs to. If not defined, use current site.
|
* @param siteId ID of the site the event belongs to. If not defined, use current site.
|
||||||
* @return Promise resolved when the notification is updated.
|
* @return Promise resolved when the notification is updated.
|
||||||
|
* @deprecated since 4.1. Use CoreReminders.removeReminder instead.
|
||||||
*/
|
*/
|
||||||
async deleteEventReminder(id: number, siteId?: string): Promise<void> {
|
async deleteEventReminder(id: number, siteId?: string): Promise<void> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
await CoreReminders.removeReminder(id, siteId);
|
||||||
|
|
||||||
if (CoreLocalNotifications.isAvailable()) {
|
|
||||||
CoreLocalNotifications.cancel(id, AddonCalendarProvider.COMPONENT, site.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
await site.getDb().deleteRecords(REMINDERS_TABLE, { id: id });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -865,7 +821,7 @@ export class AddonCalendarProvider {
|
||||||
}
|
}
|
||||||
const response: AddonCalendarCalendarDay = await site.read('core_calendar_get_calendar_day_view', params, preSets);
|
const response: AddonCalendarCalendarDay = await site.read('core_calendar_get_calendar_day_view', params, preSets);
|
||||||
this.storeEventsInLocalDB(response.events, { siteId });
|
this.storeEventsInLocalDB(response.events, { siteId });
|
||||||
this.scheduleEventsNotifications(response.events, siteId);
|
this.updateEventsReminders(response.events, site.getId());
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
@ -909,14 +865,16 @@ export class AddonCalendarProvider {
|
||||||
/**
|
/**
|
||||||
* Get a calendar reminders from local Db.
|
* Get a calendar reminders from local Db.
|
||||||
*
|
*
|
||||||
* @param id Event ID.
|
* @param eventId Event ID.
|
||||||
* @param siteId ID of the site the event belongs to. If not defined, use current site.
|
* @param siteId ID of the site the event belongs to. If not defined, use current site.
|
||||||
* @return Promise resolved when the event data is retrieved.
|
* @return Promise resolved when the event data is retrieved.
|
||||||
|
* @deprecated since 4.1. Use CoreReminders.getReminders instead.
|
||||||
*/
|
*/
|
||||||
async getEventReminders(id: number, siteId?: string): Promise<AddonCalendarReminderDBRecord[]> {
|
async getEventReminders(eventId: number, siteId?: string): Promise<CoreReminderDBRecord[]> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
return CoreReminders.getReminders({
|
||||||
|
instanceId: eventId,
|
||||||
return site.getDb().getRecords(REMINDERS_TABLE, { eventid: id }, 'timecreated ASC, time ASC');
|
component: AddonCalendarProvider.COMPONENT,
|
||||||
|
}, siteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1070,7 +1028,7 @@ export class AddonCalendarProvider {
|
||||||
response.weeks.forEach((week) => {
|
response.weeks.forEach((week) => {
|
||||||
week.days.forEach((day) => {
|
week.days.forEach((day) => {
|
||||||
this.storeEventsInLocalDB(day.events, { siteId });
|
this.storeEventsInLocalDB(day.events, { siteId });
|
||||||
this.scheduleEventsNotifications(day.events, siteId);
|
this.updateEventsReminders(day.events, site.getId());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1123,26 +1081,10 @@ export class AddonCalendarProvider {
|
||||||
* @param unit Unit.
|
* @param unit Unit.
|
||||||
* @param addDefaultLabel Whether to add the "Default" text.
|
* @param addDefaultLabel Whether to add the "Default" text.
|
||||||
* @return Translated label.
|
* @return Translated label.
|
||||||
|
* @deprecated since 4.1 Use CoreReminders.getUnitValueLabel instead.
|
||||||
*/
|
*/
|
||||||
getUnitValueLabel(value: number, unit: AddonCalendarReminderUnits, addDefaultLabel = false): string {
|
getUnitValueLabel(value: number, unit: CoreRemindersUnits, addDefaultLabel = false): string {
|
||||||
if (value === 0) {
|
return CoreReminders.getUnitValueLabel(value, unit, addDefaultLabel);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1184,7 +1126,7 @@ export class AddonCalendarProvider {
|
||||||
|
|
||||||
const response = await site.read<AddonCalendarUpcoming>('core_calendar_get_calendar_upcoming_view', params, preSets);
|
const response = await site.read<AddonCalendarUpcoming>('core_calendar_get_calendar_upcoming_view', params, preSets);
|
||||||
this.storeEventsInLocalDB(response.events, { siteId });
|
this.storeEventsInLocalDB(response.events, { siteId });
|
||||||
this.scheduleEventsNotifications(response.events, siteId);
|
this.updateEventsReminders(response.events, site.getId());
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
@ -1422,6 +1364,16 @@ export class AddonCalendarProvider {
|
||||||
return this.isCalendarDisabledInSite(site);
|
return this.isCalendarDisabledInSite(site);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next events for all the sites and schedules their notifications.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
* @deprecated since 4.1 Use AddonCalendar.updateAllSitesEventReminders.
|
||||||
|
*/
|
||||||
|
async scheduleAllSitesEventsNotifications(): Promise<void> {
|
||||||
|
await AddonCalendar.updateAllSitesEventReminders();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the next events for all the sites and schedules their notifications.
|
* Get the next events for all the sites and schedules their notifications.
|
||||||
* If an event notification time is 0, cancel its scheduled notification (if any).
|
* If an event notification time is 0, cancel its scheduled notification (if any).
|
||||||
|
@ -1429,95 +1381,36 @@ export class AddonCalendarProvider {
|
||||||
*
|
*
|
||||||
* @return Promise resolved when all the notifications have been scheduled.
|
* @return Promise resolved when all the notifications have been scheduled.
|
||||||
*/
|
*/
|
||||||
async scheduleAllSitesEventsNotifications(): Promise<void> {
|
async updateAllSitesEventReminders(): Promise<void> {
|
||||||
await CorePlatform.ready();
|
await CorePlatform.ready();
|
||||||
|
|
||||||
const notificationsEnabled = CoreLocalNotifications.isAvailable();
|
|
||||||
|
|
||||||
const siteIds = await CoreSites.getSitesIds();
|
const siteIds = await CoreSites.getSitesIds();
|
||||||
|
|
||||||
const promises = siteIds.map((siteId: string) => async () => {
|
await Promise.all(siteIds.map((siteId: string) => async () => {
|
||||||
if (notificationsEnabled) {
|
|
||||||
// Check if calendar is disabled for the site.
|
// Check if calendar is disabled for the site.
|
||||||
const disabled = await this.isDisabled(siteId);
|
const disabled = await this.isDisabled(siteId);
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
// Get first events.
|
// Get first events.
|
||||||
const events = await this.getEventsList(undefined, undefined, undefined, siteId);
|
const events = await this.getEventsList(undefined, undefined, undefined, siteId);
|
||||||
await this.scheduleEventsNotifications(events, siteId);
|
await this.updateEventsReminders(events, siteId);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}));
|
||||||
return;
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedules an event notification. If time is 0, cancel scheduled notification if any.
|
* Get the next events for all the sites and schedules their notifications.
|
||||||
* If local notification plugin is not enabled, resolve the promise.
|
|
||||||
*
|
*
|
||||||
* @param event Event to schedule.
|
* @return Promise resolved when done.
|
||||||
* @param reminderId The reminder ID.
|
* @deprecated since 4.1. No replacement for that function.
|
||||||
* @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(
|
async scheduleEventsNotifications(
|
||||||
event: { id: number; timestart: number; name: string},
|
events: ({ id: number; timestart: number; timeduration: number; name: string})[],
|
||||||
reminderId: number,
|
|
||||||
time?: number | null,
|
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|
||||||
if (!CoreLocalNotifications.isAvailable()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
if (time === 0) {
|
await AddonCalendar.updateEventsReminders(events, siteId);
|
||||||
// Cancel if it was scheduled.
|
|
||||||
return CoreLocalNotifications.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!time) {
|
|
||||||
// Get event default time to calculate the notification time.
|
|
||||||
time = await this.getDefaultNotificationTime(siteId);
|
|
||||||
|
|
||||||
if (time === 0) {
|
|
||||||
// Default notification time is disabled, do not show.
|
|
||||||
return CoreLocalNotifications.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
time = event.timestart - time;
|
|
||||||
}
|
|
||||||
|
|
||||||
time = time * 1000;
|
|
||||||
|
|
||||||
if (time <= Date.now()) {
|
|
||||||
// This reminder is over, don't schedule. Cancel if it was scheduled.
|
|
||||||
return CoreLocalNotifications.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const notificationData: AddonCalendarPushNotificationData = {
|
|
||||||
eventId: event.id,
|
|
||||||
reminderId: reminderId,
|
|
||||||
siteId: siteId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const notification: ILocalNotification = {
|
|
||||||
id: reminderId,
|
|
||||||
title: event.name,
|
|
||||||
text: CoreTimeUtils.userDate(event.timestart * 1000, 'core.strftimedaydatetime', true),
|
|
||||||
icon: 'file://assets/img/icons/calendar.png',
|
|
||||||
trigger: {
|
|
||||||
at: new Date(time),
|
|
||||||
},
|
|
||||||
data: notificationData,
|
|
||||||
};
|
|
||||||
|
|
||||||
return CoreLocalNotifications.schedule(notification, AddonCalendarProvider.COMPONENT, siteId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1526,38 +1419,38 @@ export class AddonCalendarProvider {
|
||||||
* If local notification plugin is not enabled, resolve the promise.
|
* If local notification plugin is not enabled, resolve the promise.
|
||||||
*
|
*
|
||||||
* @param events Events to schedule.
|
* @param events Events to schedule.
|
||||||
* @param siteId ID of the site the events belong to. If not defined, use current site.
|
* @param siteId ID of the site the events belong to.
|
||||||
* @return Promise resolved when all the notifications have been scheduled.
|
* @return Promise resolved when all the notifications have been scheduled.
|
||||||
*/
|
*/
|
||||||
async scheduleEventsNotifications(
|
protected async updateEventsReminders(
|
||||||
events: ({ id: number; timestart: number; timeduration: number; name: string})[],
|
events: ({ id: number; timestart: number; name: string})[],
|
||||||
siteId?: string,
|
siteId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
await Promise.all(events.map(async (event) => {
|
||||||
if (!CoreLocalNotifications.isAvailable()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
|
||||||
|
|
||||||
const promises = events.map(async (event) => {
|
|
||||||
if (event.timestart * 1000 <= Date.now()) {
|
if (event.timestart * 1000 <= Date.now()) {
|
||||||
// The event has already started, don't schedule it.
|
// The event has already started, don't schedule it.
|
||||||
return;
|
|
||||||
|
// @TODO Decide when to completelly remove expired events.
|
||||||
|
return CoreReminders.cancelReminder(event.id, AddonCalendarProvider.COMPONENT, siteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const reminders = await this.getEventReminders(event.id, siteId);
|
const reminders = await CoreReminders.getReminders({
|
||||||
|
instanceId: event.id,
|
||||||
|
component: AddonCalendarProvider.COMPONENT,
|
||||||
|
}, siteId);
|
||||||
|
|
||||||
const p2 = reminders.map((reminder) => {
|
await Promise.all(reminders.map(async (reminder) => {
|
||||||
const time = reminder.time ? event.timestart - reminder.time : reminder.time;
|
if (reminder.time !== event.timestart || reminder.title !== event.name) {
|
||||||
|
reminder.time = event.timestart;
|
||||||
|
reminder.title = event.name;
|
||||||
|
|
||||||
return this.scheduleEventNotification(event, reminder.id, time, siteId);
|
CoreReminders.updateReminder(
|
||||||
});
|
reminder,
|
||||||
|
siteId,
|
||||||
await Promise.all(p2);
|
);
|
||||||
});
|
}
|
||||||
|
}));
|
||||||
await Promise.all(promises);
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1566,13 +1459,10 @@ export class AddonCalendarProvider {
|
||||||
* @param time New default time.
|
* @param time New default time.
|
||||||
* @param siteId ID of the site. If not defined, use current site.
|
* @param siteId ID of the site. If not defined, use current site.
|
||||||
* @return Promise resolved when stored.
|
* @return Promise resolved when stored.
|
||||||
|
* @deprecated since 4.1 Use CoreReminders.setDefaultNotificationTime.
|
||||||
*/
|
*/
|
||||||
async setDefaultNotificationTime(time: number, siteId?: string): Promise<void> {
|
async setDefaultNotificationTime(time: number, siteId?: string): Promise<void> {
|
||||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
await CoreReminders.setDefaultNotificationTime(time, siteId);
|
||||||
|
|
||||||
const key = AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId;
|
|
||||||
|
|
||||||
await CoreConfig.set(key, time);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1587,24 +1477,8 @@ export class AddonCalendarProvider {
|
||||||
options: AddonCalendarStoreEventsOptions = {},
|
options: AddonCalendarStoreEventsOptions = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const site = await CoreSites.getSite(options.siteId);
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
const siteId = site.getId();
|
|
||||||
const addDefaultReminder = options.addDefaultReminder ?? true;
|
const addDefaultReminder = options.addDefaultReminder ?? true;
|
||||||
|
|
||||||
if (addDefaultReminder) {
|
|
||||||
// Add default reminder if the event isn't stored already and doesn't have any reminder.
|
|
||||||
try {
|
|
||||||
await this.getEventFromLocalDb(event.id, siteId);
|
|
||||||
} catch {
|
|
||||||
// Event does not exist.
|
|
||||||
const reminders = await this.getEventReminders(event.id, siteId);
|
|
||||||
|
|
||||||
if (reminders.length === 0) {
|
|
||||||
// No reminders, create the default one.
|
|
||||||
this.addEventReminder(event, undefined, siteId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't store data that can be calculated like formattedtime, iscategoryevent, etc.
|
// Don't store data that can be calculated like formattedtime, iscategoryevent, etc.
|
||||||
let eventRecord: AddonCalendarEventDBRecord = {
|
let eventRecord: AddonCalendarEventDBRecord = {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
|
@ -1659,9 +1533,40 @@ export class AddonCalendarProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (addDefaultReminder) {
|
||||||
|
this.addDefaultEventReminder(eventRecord, site.getId());
|
||||||
|
}
|
||||||
|
|
||||||
await site.getDb().insertRecord(EVENTS_TABLE, eventRecord);
|
await site.getDb().insertRecord(EVENTS_TABLE, eventRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the default event reminder.
|
||||||
|
*
|
||||||
|
* @param event Event to add the reminder to.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
*/
|
||||||
|
protected async addDefaultEventReminder(event: AddonCalendarEventDBRecord, siteId?: string): Promise<void> {
|
||||||
|
// Add default reminder if the event isn't stored already and doesn't have any reminder.
|
||||||
|
const eventExist = await CoreUtils.promiseWorks(this.getEventFromLocalDb(event.id, siteId));
|
||||||
|
if (eventExist) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reminders = await CoreReminders.getReminders({
|
||||||
|
instanceId: event.id,
|
||||||
|
component: AddonCalendarProvider.COMPONENT,
|
||||||
|
}, siteId);
|
||||||
|
|
||||||
|
if (reminders.length > 0) {
|
||||||
|
// It already has reminders.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No reminders, create the default one.
|
||||||
|
await this.addEventReminder(event, undefined, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store events in local DB.
|
* Store events in local DB.
|
||||||
*
|
*
|
||||||
|
@ -1681,9 +1586,7 @@ export class AddonCalendarProvider {
|
||||||
*
|
*
|
||||||
* @param eventId ID of the event. Negative value to edit offline 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 formData Form data.
|
||||||
* @param timeCreated The time the event was created. Only if modifying a new offline event.
|
* @param options Calendar submit event options.
|
||||||
* @param forceOffline True to always save it in offline.
|
|
||||||
* @param siteId Site ID. If not defined, current site.
|
|
||||||
* @return Promise resolved with the event and a boolean indicating if data was sent to server or stored in offline.
|
* @return Promise resolved with the event and a boolean indicating if data was sent to server or stored in offline.
|
||||||
*/
|
*/
|
||||||
async submitEvent(
|
async submitEvent(
|
||||||
|
@ -1701,7 +1604,8 @@ export class AddonCalendarProvider {
|
||||||
// Now save the reminders if any.
|
// Now save the reminders if any.
|
||||||
if (options.reminders?.length) {
|
if (options.reminders?.length) {
|
||||||
await CoreUtils.ignoreErrors(
|
await CoreUtils.ignoreErrors(
|
||||||
Promise.all(options.reminders.map((reminder) => this.addEventReminder(event, reminder.time, siteId))),
|
Promise.all(options.reminders.map((reminder) =>
|
||||||
|
this.addEventReminder(event, reminder.time, siteId))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1723,7 +1627,8 @@ export class AddonCalendarProvider {
|
||||||
// Now save the reminders if any.
|
// Now save the reminders if any.
|
||||||
if (options.reminders?.length) {
|
if (options.reminders?.length) {
|
||||||
await CoreUtils.ignoreErrors(
|
await CoreUtils.ignoreErrors(
|
||||||
Promise.all(options.reminders.map((reminder) => this.addEventReminder(event, reminder.time, siteId))),
|
Promise.all(options.reminders.map((reminder) =>
|
||||||
|
this.addEventReminder(event, reminder.time, siteId))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1753,6 +1658,7 @@ export class AddonCalendarProvider {
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<AddonCalendarEvent> {
|
): Promise<AddonCalendarEvent> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
siteId = site.getId();
|
||||||
|
|
||||||
// Add data that is "hidden" in web.
|
// Add data that is "hidden" in web.
|
||||||
formData.id = eventId > 0 ? eventId : 0;
|
formData.id = eventId > 0 ? eventId : 0;
|
||||||
|
@ -1780,9 +1686,16 @@ export class AddonCalendarProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventId < 0) {
|
if (eventId < 0) {
|
||||||
// Offline event has been sent. Change reminders eventid if any.
|
// Offline event has been sent. Change reminders instanceId if any.
|
||||||
await CoreUtils.ignoreErrors(
|
await CoreUtils.ignoreErrors(
|
||||||
site.getDb().updateRecords(REMINDERS_TABLE, { eventid: result.event.id }, { eventid: eventId }),
|
CoreReminders.updateReminders(
|
||||||
|
{ instanceId: result.event.id },
|
||||||
|
{
|
||||||
|
instanceId: eventId,
|
||||||
|
component: AddonCalendarProvider.COMPONENT,
|
||||||
|
},
|
||||||
|
siteId,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2316,29 +2229,19 @@ export type AddonCalendarUpdatedEventEvent = {
|
||||||
sent?: boolean;
|
sent?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Additional data sent in push notifications, with some calculated data.
|
|
||||||
*/
|
|
||||||
type AddonCalendarPushNotificationData = {
|
|
||||||
eventId: number;
|
|
||||||
reminderId: number;
|
|
||||||
siteId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Value and unit for reminders.
|
* Value and unit for reminders.
|
||||||
|
*
|
||||||
|
* @deprecated since 4.1, use CoreReminderValueAndUnit instead.
|
||||||
*/
|
*/
|
||||||
export type AddonCalendarValueAndUnit = {
|
export type AddonCalendarValueAndUnit = CoreReminderValueAndUnit;
|
||||||
value: number;
|
|
||||||
unit: AddonCalendarReminderUnits;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options to pass to submit event.
|
* Options to pass to submit event.
|
||||||
*/
|
*/
|
||||||
export type AddonCalendarSubmitEventOptions = {
|
export type AddonCalendarSubmitEventOptions = {
|
||||||
reminders?: {
|
reminders?: {
|
||||||
time: number | null;
|
time?: number;
|
||||||
}[];
|
}[];
|
||||||
forceOffline?: boolean;
|
forceOffline?: boolean;
|
||||||
siteId?: string; // Site ID. If not defined, current site.
|
siteId?: string; // Site ID. If not defined, current site.
|
||||||
|
|
|
@ -13,19 +13,19 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { SQLiteDB } from '@classes/sqlitedb';
|
import { SQLiteDB } from '@classes/sqlitedb';
|
||||||
|
import { CoreRemindersService, CoreReminders } from '@features/reminders/services/reminders';
|
||||||
import { CoreConfig } from '@services/config';
|
import { CoreConfig } from '@services/config';
|
||||||
import { CoreSiteSchema } from '@services/sites';
|
import { CoreSiteSchema } from '@services/sites';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { AddonCalendar, AddonCalendarEventType, AddonCalendarProvider } from '../calendar';
|
import { AddonCalendarEventType } from '../calendar';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database variables for AddonCalendarProvider service.
|
* Database variables for AddonCalendarProvider service.
|
||||||
*/
|
*/
|
||||||
export const EVENTS_TABLE = 'addon_calendar_events_3';
|
export const EVENTS_TABLE = 'addon_calendar_events_3';
|
||||||
export const REMINDERS_TABLE = 'addon_calendar_reminders_2';
|
|
||||||
export const CALENDAR_SITE_SCHEMA: CoreSiteSchema = {
|
export const CALENDAR_SITE_SCHEMA: CoreSiteSchema = {
|
||||||
name: 'AddonCalendarProvider',
|
name: 'AddonCalendarProvider',
|
||||||
version: 4,
|
version: 5,
|
||||||
canBeCleared: [EVENTS_TABLE],
|
canBeCleared: [EVENTS_TABLE],
|
||||||
tables: [
|
tables: [
|
||||||
{
|
{
|
||||||
|
@ -179,105 +179,41 @@ export const CALENDAR_SITE_SCHEMA: CoreSiteSchema = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: REMINDERS_TABLE,
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'id',
|
|
||||||
type: 'INTEGER',
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'eventid',
|
|
||||||
type: 'INTEGER',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'time',
|
|
||||||
type: 'INTEGER',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'timecreated',
|
|
||||||
type: 'INTEGER',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
uniqueKeys: [
|
|
||||||
['eventid', 'time'],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
async migrate(db: SQLiteDB, oldVersion: number, siteId: string): Promise<void> {
|
async migrate(db: SQLiteDB, oldVersion: number, siteId: string): Promise<void> {
|
||||||
if (oldVersion < 3) {
|
if (oldVersion < 5) {
|
||||||
// Migrate calendar events. New format @since 3.7.
|
await migrateDefaultTime(siteId, oldVersion < 4);
|
||||||
let oldTable = 'addon_calendar_events_2';
|
|
||||||
|
|
||||||
try {
|
|
||||||
await db.tableExists(oldTable);
|
|
||||||
} catch {
|
|
||||||
// The v2 table doesn't exist, try with v1.
|
|
||||||
oldTable = 'addon_calendar_events';
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.migrateTable(oldTable, EVENTS_TABLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldVersion < 4) {
|
|
||||||
// Migrate default notification time if it was changed.
|
|
||||||
// Don't use getDefaultNotificationTime to be able to detect if the value was changed or not.
|
|
||||||
const key = AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId;
|
|
||||||
const defaultTime = await CoreUtils.ignoreErrors(CoreConfig.get(key, null));
|
|
||||||
|
|
||||||
if (defaultTime) {
|
|
||||||
// Convert from minutes to seconds.
|
|
||||||
AddonCalendar.setDefaultNotificationTime(defaultTime * 60, siteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate reminders. New format @since 4.0.
|
|
||||||
const oldTable = 'addon_calendar_reminders';
|
|
||||||
|
|
||||||
try {
|
|
||||||
await db.tableExists(oldTable);
|
|
||||||
} catch (error) {
|
|
||||||
// Old table does not exist, ignore.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const records = await db.getAllRecords<AddonCalendarReminderDBRecord>(oldTable);
|
|
||||||
const events: Record<number, AddonCalendarEventDBRecord> = {};
|
|
||||||
|
|
||||||
await Promise.all(records.map(async (record) => {
|
|
||||||
// Get the event to compare the reminder time with the event time.
|
|
||||||
if (!events[record.eventid]) {
|
|
||||||
try {
|
|
||||||
events[record.eventid] = await db.getRecord(EVENTS_TABLE, { id: record.eventid });
|
|
||||||
} catch {
|
|
||||||
// Event not found in local DB, shouldn't happen. Ignore the reminder.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!record.time || record.time === -1) {
|
|
||||||
// Default reminder. Use null now.
|
|
||||||
record.time = null;
|
|
||||||
} else if (record.time > events[record.eventid].timestart) {
|
|
||||||
// Reminder is after the event, ignore it.
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// Remove seconds from the old reminder, it could include seconds by mistake.
|
|
||||||
record.time = events[record.eventid].timestart - Math.floor(record.time / 60) * 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.insertRecord(REMINDERS_TABLE, record);
|
|
||||||
}));
|
|
||||||
|
|
||||||
try {
|
|
||||||
await db.dropTable(oldTable);
|
|
||||||
} catch (error) {
|
|
||||||
// Error deleting old table, ignore.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate default notification time if it was changed.
|
||||||
|
* Don't use getDefaultNotificationTime to be able to detect if the value was changed or not.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID to migrate.
|
||||||
|
* @param convertToSeconds If true, time will be converted to seconds.
|
||||||
|
*/
|
||||||
|
const migrateDefaultTime = async (siteId: string, convertToSeconds = false): Promise<void> => {
|
||||||
|
|
||||||
|
const key = 'mmaCalendarDefaultNotifTime#' + siteId;
|
||||||
|
try {
|
||||||
|
let defaultTime = await CoreConfig.get<number>(key);
|
||||||
|
await CoreUtils.ignoreErrors(CoreConfig.delete(key));
|
||||||
|
|
||||||
|
if (defaultTime <= 0) {
|
||||||
|
defaultTime = CoreRemindersService.DISABLED;
|
||||||
|
} else if (convertToSeconds) {
|
||||||
|
// Convert from minutes to seconds.
|
||||||
|
defaultTime = defaultTime * 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreReminders.setDefaultNotificationTime(defaultTime, siteId);
|
||||||
|
} catch {
|
||||||
|
// Ignore errors, already migrated.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export type AddonCalendarEventDBRecord = {
|
export type AddonCalendarEventDBRecord = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -318,10 +254,3 @@ export type AddonCalendarEventDBRecord = {
|
||||||
maxdaytimestamp?: number;
|
maxdaytimestamp?: number;
|
||||||
draggable?: number;
|
draggable?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AddonCalendarReminderDBRecord = {
|
|
||||||
id: number;
|
|
||||||
eventid: number;
|
|
||||||
time: number | null; // Number of seconds before the event, null for default time.
|
|
||||||
timecreated?: number | null;
|
|
||||||
};
|
|
||||||
|
|
|
@ -98,7 +98,6 @@ export class CoreContentLinksHelperProvider {
|
||||||
* @param pageName Name of the page to go.
|
* @param pageName Name of the page to go.
|
||||||
* @param pageParams Params to send to the page.
|
* @param pageParams Params to send to the page.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @param checkMenu If true, check if the root page of a main menu tab. Only the page name will be checked.
|
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
* @deprecated since 3.9.5. Use CoreNavigator.navigateToSitePath instead.
|
* @deprecated since 3.9.5. Use CoreNavigator.navigateToSitePath instead.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { CoreCourseModuleManualCompletionComponent } from './module-manual-compl
|
||||||
import { CoreCourseModuleNavigationComponent } from './module-navigation/module-navigation';
|
import { CoreCourseModuleNavigationComponent } from './module-navigation/module-navigation';
|
||||||
import { CoreCourseModuleSummaryComponent } from './module-summary/module-summary';
|
import { CoreCourseModuleSummaryComponent } from './module-summary/module-summary';
|
||||||
import { CoreCourseCourseIndexTourComponent } from './course-index-tour/course-index-tour';
|
import { CoreCourseCourseIndexTourComponent } from './course-index-tour/course-index-tour';
|
||||||
|
import { CoreRemindersComponentsModule } from '@features/reminders/components/components.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -48,6 +49,7 @@ import { CoreCourseCourseIndexTourComponent } from './course-index-tour/course-i
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CoreBlockComponentsModule,
|
CoreBlockComponentsModule,
|
||||||
|
CoreRemindersComponentsModule,
|
||||||
CoreSharedModule,
|
CoreSharedModule,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
|
|
|
@ -26,10 +26,9 @@
|
||||||
|
|
||||||
<!-- Activity dates. -->
|
<!-- Activity dates. -->
|
||||||
<div *ngIf="module.dates && module.dates.length" class="core-module-dates core-module-info-box-section">
|
<div *ngIf="module.dates && module.dates.length" class="core-module-dates core-module-info-box-section">
|
||||||
<p *ngFor="let date of module.dates">
|
<core-reminders-date *ngFor="let date of module.dates" [component]="component" [instanceId]="module.id" [type]="date.dataid"
|
||||||
<ion-icon name="fas-calendar" aria-hidden="true"></ion-icon><strong>{{ date.label }}</strong>
|
[label]="date.label" [time]="date.timestamp" [relativeTo]="date.relativeto" [title]="module.name" [url]="module.url">
|
||||||
{{ date.readableTime }}
|
</core-reminders-date>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Availability info space. -->
|
<!-- Availability info space. -->
|
||||||
|
|
|
@ -48,12 +48,6 @@
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-module-dates ion-icon {
|
|
||||||
margin-left: 4px;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.core-module-dates,
|
|
||||||
.core-module-availabilityinfo {
|
.core-module-availabilityinfo {
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
ion-icon {
|
ion-icon {
|
||||||
|
@ -94,8 +88,6 @@
|
||||||
white-space: normal !important;
|
white-space: normal !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:host-context(.core-iframe-fullscreen) {
|
:host-context(.core-iframe-fullscreen) {
|
||||||
|
|
|
@ -81,10 +81,9 @@
|
||||||
*ngIf="(showActivityDates && module.dates && module.dates.length) || module.availabilityinfo">
|
*ngIf="(showActivityDates && module.dates && module.dates.length) || module.availabilityinfo">
|
||||||
<!-- Activity dates. -->
|
<!-- Activity dates. -->
|
||||||
<div *ngIf="showActivityDates && module.dates && module.dates.length" class="core-module-dates">
|
<div *ngIf="showActivityDates && module.dates && module.dates.length" class="core-module-dates">
|
||||||
<p *ngFor="let date of module.dates">
|
<core-reminders-date *ngFor="let date of module.dates" [type]="date.id" [label]="date.label" [time]="date.timestamp"
|
||||||
<ion-icon name="fas-calendar" aria-hidden="true"></ion-icon><strong>{{ date.label }}</strong>
|
[relativeTo]="date.relativeto">
|
||||||
{{ date.readableTime }}
|
</core-reminders-date>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Availability info -->
|
<!-- Availability info -->
|
||||||
|
|
|
@ -57,16 +57,14 @@
|
||||||
</core-progress-bar>
|
</core-progress-bar>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="course.startdate || course.enddate" class="core-course-dates">
|
<div *ngIf="course.startdate || course.enddate" class="core-course-dates">
|
||||||
<p *ngIf="course.startdate">
|
<core-reminders-date *ngIf="course.startdate" component="course" [instanceId]="course.id" type="coursestart"
|
||||||
<ion-icon name="fas-calendar" aria-hidden="true"></ion-icon>
|
[label]="'core.course.startdate' | translate" [time]="course.startdate" [title]="course.fullname"
|
||||||
<strong>{{ 'core.course.startdate' | translate }}</strong><br>
|
[url]="courseUrl">
|
||||||
{{ course.startdate * 1000 | coreFormatDate:'strftimedaydatetime' }}
|
</core-reminders-date>
|
||||||
</p>
|
<core-reminders-date *ngIf="course.enddate" component="course" [instanceId]="course.id" type="courseend"
|
||||||
<p *ngIf="course.enddate">
|
[label]="'core.course.enddate' | translate" [time]="course.enddate" [title]="course.fullname"
|
||||||
<ion-icon name="fas-calendar" aria-hidden="true"></ion-icon>
|
[url]="courseUrl">
|
||||||
<strong>{{ 'core.course.enddate' | translate }}</strong><br>
|
</core-reminders-date>
|
||||||
{{ course.enddate * 1000 | coreFormatDate:'strftimedaydatetime' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
import { CoreSharedModule } from '@/core/shared.module';
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
import { CoreCourseSummaryPage } from './course-summary';
|
import { CoreCourseSummaryPage } from './course-summary';
|
||||||
|
import { CoreRemindersComponentsModule } from '@features/reminders/components/components.module';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -27,6 +28,7 @@ const routes: Routes = [
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CoreSharedModule,
|
CoreSharedModule,
|
||||||
|
CoreRemindersComponentsModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
CoreCourseSummaryPage,
|
CoreCourseSummaryPage,
|
||||||
|
@ -39,6 +41,7 @@ export class CoreCoursePreviewPageComponentModule { }
|
||||||
RouterModule.forChild(routes),
|
RouterModule.forChild(routes),
|
||||||
CoreSharedModule,
|
CoreSharedModule,
|
||||||
CoreCoursePreviewPageComponentModule,
|
CoreCoursePreviewPageComponentModule,
|
||||||
|
CoreRemindersComponentsModule,
|
||||||
],
|
],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
})
|
})
|
||||||
|
|
|
@ -27,7 +27,6 @@ import {
|
||||||
CoreCourseModuleCompletionTracking,
|
CoreCourseModuleCompletionTracking,
|
||||||
CoreCourseModuleCompletionStatus,
|
CoreCourseModuleCompletionStatus,
|
||||||
CoreCourseGetContentsWSModule,
|
CoreCourseGetContentsWSModule,
|
||||||
CoreCourseGetContentsWSModuleDate,
|
|
||||||
} from './course';
|
} from './course';
|
||||||
import { CoreConstants } from '@/core/constants';
|
import { CoreConstants } from '@/core/constants';
|
||||||
import { CoreLogger } from '@singletons/logger';
|
import { CoreLogger } from '@singletons/logger';
|
||||||
|
@ -1189,7 +1188,7 @@ export class CoreCourseHelperProvider {
|
||||||
* This should be used in 3.6 sites or higher, where the course contents already include the completion.
|
* This should be used in 3.6 sites or higher, where the course contents already include the completion.
|
||||||
*
|
*
|
||||||
* @param courseId The course to get the completion.
|
* @param courseId The course to get the completion.
|
||||||
* @param mmodule The module.
|
* @param module The module.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
|
@ -2086,20 +2085,12 @@ export type CoreCourseSectionWithStatus = CoreCourseSection & {
|
||||||
/**
|
/**
|
||||||
* Module with calculated data.
|
* Module with calculated data.
|
||||||
*/
|
*/
|
||||||
export type CoreCourseModuleData = Omit<CoreCourseGetContentsWSModule, 'completiondata'|'dates'> & {
|
export type CoreCourseModuleData = Omit<CoreCourseGetContentsWSModule, 'completiondata'> & {
|
||||||
course: number; // The course id.
|
course: number; // The course id.
|
||||||
isStealth?: boolean;
|
isStealth?: boolean;
|
||||||
handlerData?: CoreCourseModuleHandlerData;
|
handlerData?: CoreCourseModuleHandlerData;
|
||||||
completiondata?: CoreCourseModuleCompletionData;
|
completiondata?: CoreCourseModuleCompletionData;
|
||||||
section: number;
|
section: number;
|
||||||
dates?: CoreCourseModuleDate[];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module date with calculated data.
|
|
||||||
*/
|
|
||||||
export type CoreCourseModuleDate = CoreCourseGetContentsWSModuleDate & {
|
|
||||||
readableTime: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -38,7 +38,7 @@ import {
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreWSError } from '@classes/errors/wserror';
|
import { CoreWSError } from '@classes/errors/wserror';
|
||||||
import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
|
import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
|
||||||
import { CoreCourseHelper, CoreCourseModuleData, CoreCourseModuleCompletionData, CoreCourseModuleDate } from './course-helper';
|
import { CoreCourseHelper, CoreCourseModuleData, CoreCourseModuleCompletionData } from './course-helper';
|
||||||
import { CoreCourseFormatDelegate } from './format-delegate';
|
import { CoreCourseFormatDelegate } from './format-delegate';
|
||||||
import { CoreCronDelegate } from '@services/cron';
|
import { CoreCronDelegate } from '@services/cron';
|
||||||
import { CoreCourseLogCronHandler } from './handlers/log-cron';
|
import { CoreCourseLogCronHandler } from './handlers/log-cron';
|
||||||
|
@ -53,7 +53,6 @@ import { CoreDatabaseTable } from '@classes/database/database-table';
|
||||||
import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy';
|
import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy';
|
||||||
import { SQLiteDB } from '@classes/sqlitedb';
|
import { SQLiteDB } from '@classes/sqlitedb';
|
||||||
import { CorePlatform } from '@services/platform';
|
import { CorePlatform } from '@services/platform';
|
||||||
import { CoreTime } from '@singletons/time';
|
|
||||||
import { asyncObservable, firstValueFrom } from '@/core/utils/rxjs';
|
import { asyncObservable, firstValueFrom } from '@/core/utils/rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@ -678,39 +677,11 @@ export class CoreCourseProvider {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let formattedDates: CoreCourseModuleDate[] | undefined;
|
|
||||||
|
|
||||||
if (module.dates) {
|
|
||||||
formattedDates = module.dates.map(date => {
|
|
||||||
let readableTime = '';
|
|
||||||
if (!date.relativeto) {
|
|
||||||
readableTime = CoreTimeUtils.userDate(date.timestamp * 1000, 'core.strftimedatetime', true);
|
|
||||||
} else {
|
|
||||||
readableTime = Translate.instant(
|
|
||||||
'core.course.relativedatessubmissionduedate' + (date.timestamp > date.relativeto ? 'after' : 'before'),
|
|
||||||
{
|
|
||||||
$a: {
|
|
||||||
datediffstr: date.relativeto === date.timestamp ?
|
|
||||||
'0 ' + Translate.instant('core.secs') :
|
|
||||||
CoreTime.formatTime(date.relativeto - date.timestamp, 3),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...date,
|
|
||||||
readableTime,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...module,
|
...module,
|
||||||
course: courseId,
|
course: courseId,
|
||||||
section: sectionId,
|
section: sectionId,
|
||||||
completiondata: completionData,
|
completiondata: completionData,
|
||||||
dates: formattedDates,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1775,7 +1746,10 @@ export type CoreCourseGetContentsWSModule = {
|
||||||
completiondata?: CoreCourseModuleWSCompletionData; // Module completion data.
|
completiondata?: CoreCourseModuleWSCompletionData; // Module completion data.
|
||||||
contents?: CoreCourseModuleContentFile[];
|
contents?: CoreCourseModuleContentFile[];
|
||||||
downloadcontent?: number; // @since 4.0 The download content value.
|
downloadcontent?: number; // @since 4.0 The download content value.
|
||||||
dates?: CoreCourseGetContentsWSModuleDate[]; // @since 3.11. Activity dates.
|
dates?: {
|
||||||
|
label: string;
|
||||||
|
timestamp: number;
|
||||||
|
}[]; // @since 3.11. Activity dates.
|
||||||
contentsinfo?: { // @since v3.7.6 Contents summary information.
|
contentsinfo?: { // @since v3.7.6 Contents summary information.
|
||||||
filescount: number; // Total number of files.
|
filescount: number; // Total number of files.
|
||||||
filessize: number; // Total files size.
|
filessize: number; // Total files size.
|
||||||
|
@ -1785,16 +1759,6 @@ export type CoreCourseGetContentsWSModule = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity date.
|
|
||||||
*/
|
|
||||||
export type CoreCourseGetContentsWSModuleDate = {
|
|
||||||
label: string;
|
|
||||||
timestamp: number;
|
|
||||||
relativeto?: number; // @since 4.1. Relative date timestamp.
|
|
||||||
dataid?: string; // @since 4.1. ID to identify the text.
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data returned by core_course_get_contents WS.
|
* Data returned by core_course_get_contents WS.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { FileOpener } from '@ionic-native/file-opener/ngx';
|
||||||
import { FileTransfer } from '@ionic-native/file-transfer/ngx';
|
import { FileTransfer } from '@ionic-native/file-transfer/ngx';
|
||||||
import { Geolocation } from '@ionic-native/geolocation/ngx';
|
import { Geolocation } from '@ionic-native/geolocation/ngx';
|
||||||
import { InAppBrowser } from '@ionic-native/in-app-browser/ngx';
|
import { InAppBrowser } from '@ionic-native/in-app-browser/ngx';
|
||||||
|
import { LocalNotifications } from '@ionic-native/local-notifications/ngx';
|
||||||
import { MediaCapture } from '@ionic-native/media-capture/ngx';
|
import { MediaCapture } from '@ionic-native/media-capture/ngx';
|
||||||
import { Zip } from '@ionic-native/zip/ngx';
|
import { Zip } from '@ionic-native/zip/ngx';
|
||||||
|
|
||||||
|
@ -36,9 +37,11 @@ import { FileOpenerMock } from './services/file-opener';
|
||||||
import { FileTransferMock } from './services/file-transfer';
|
import { FileTransferMock } from './services/file-transfer';
|
||||||
import { GeolocationMock } from './services/geolocation';
|
import { GeolocationMock } from './services/geolocation';
|
||||||
import { InAppBrowserMock } from './services/inappbrowser';
|
import { InAppBrowserMock } from './services/inappbrowser';
|
||||||
|
import { LocalNotificationsMock } from './services/local-notifications';
|
||||||
import { MediaCaptureMock } from './services/media-capture';
|
import { MediaCaptureMock } from './services/media-capture';
|
||||||
import { ZipMock } from './services/zip';
|
import { ZipMock } from './services/zip';
|
||||||
import { CorePlatform } from '@services/platform';
|
import { CorePlatform } from '@services/platform';
|
||||||
|
import { CoreLocalNotifications } from '@services/local-notifications';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This module handles the emulation of Cordova plugins in browser and desktop.
|
* This module handles the emulation of Cordova plugins in browser and desktop.
|
||||||
|
@ -90,6 +93,12 @@ import { CorePlatform } from '@services/platform';
|
||||||
provide: Zip,
|
provide: Zip,
|
||||||
useFactory: (): Zip => CorePlatform.is('cordova') ? new Zip() : new ZipMock(),
|
useFactory: (): Zip => CorePlatform.is('cordova') ? new Zip() : new ZipMock(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: LocalNotifications,
|
||||||
|
useFactory: (): LocalNotifications => CoreLocalNotifications.isPluginAvailable()
|
||||||
|
? new LocalNotifications()
|
||||||
|
: new LocalNotificationsMock(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: APP_INITIALIZER,
|
provide: APP_INITIALIZER,
|
||||||
useFactory: () => () => {
|
useFactory: () => () => {
|
||||||
|
|
|
@ -0,0 +1,407 @@
|
||||||
|
// (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 { CoreError } from '@classes/errors/error';
|
||||||
|
import { ILocalNotification, ILocalNotificationAction, LocalNotifications } from '@ionic-native/local-notifications/ngx';
|
||||||
|
import { Observable, Subject } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock LocalNotifications service.
|
||||||
|
*/
|
||||||
|
export class LocalNotificationsMock extends LocalNotifications {
|
||||||
|
|
||||||
|
protected scheduledNotifications: ILocalNotification[] = [];
|
||||||
|
protected triggeredNotifications: ILocalNotification[] = [];
|
||||||
|
protected presentNotifications: Record<number, Notification> = {};
|
||||||
|
protected nextTimeout = 0;
|
||||||
|
protected hasGranted?: boolean;
|
||||||
|
protected observables = {
|
||||||
|
trigger: new Subject<ILocalNotification>(),
|
||||||
|
click: new Subject<ILocalNotification>(),
|
||||||
|
clear: new Subject<Notification>(),
|
||||||
|
clearall: new Subject<void>(),
|
||||||
|
cancel: new Subject<ILocalNotification>(),
|
||||||
|
cancelall: new Subject<void>(),
|
||||||
|
schedule: new Subject<ILocalNotification>(),
|
||||||
|
update: new Subject<ILocalNotification>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
schedule(options?: ILocalNotification | Array<ILocalNotification>): void {
|
||||||
|
this.hasPermission().then(() => {
|
||||||
|
// Do not check permission here, it could be denied by Selenium.
|
||||||
|
if (!options) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(options)) {
|
||||||
|
options = [options];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scheduledNotifications = this.scheduledNotifications.concat(options);
|
||||||
|
this.scheduledNotifications.sort((a, b) =>
|
||||||
|
(a.trigger?.at?.getTime() || 0) - (b.trigger?.at?.getTime() || 0));
|
||||||
|
|
||||||
|
options.forEach((notification) => {
|
||||||
|
this.observables.schedule.next(notification);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.scheduleNotifications();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}).catch(() => {
|
||||||
|
// Ignore errors.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets timeout for next nofitication.
|
||||||
|
*/
|
||||||
|
protected scheduleNotifications(): void {
|
||||||
|
window.clearTimeout(this.nextTimeout);
|
||||||
|
|
||||||
|
const nextNotification = this.scheduledNotifications[0];
|
||||||
|
if (!nextNotification) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notificationTime = nextNotification.trigger?.at?.getTime() || 0;
|
||||||
|
const timeout = notificationTime - Date.now();
|
||||||
|
if (timeout <= 0) {
|
||||||
|
this.triggerNextNotification();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nextTimeout = window.setTimeout(() => {
|
||||||
|
this.triggerNextNotification();
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the next notification.
|
||||||
|
*/
|
||||||
|
protected triggerNextNotification(): void {
|
||||||
|
const dateNow = Date.now();
|
||||||
|
|
||||||
|
const nextNotification = this.scheduledNotifications[0];
|
||||||
|
if (!nextNotification) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notificationTime = nextNotification.trigger?.at?.getTime() || 0;
|
||||||
|
if (notificationTime === 0 || notificationTime <= dateNow) {
|
||||||
|
const body = Array.isArray(nextNotification.text) ? nextNotification.text.join() : nextNotification.text;
|
||||||
|
const notification = new Notification(nextNotification.title || '', {
|
||||||
|
body,
|
||||||
|
data: nextNotification.data,
|
||||||
|
icon: nextNotification.icon,
|
||||||
|
requireInteraction: true,
|
||||||
|
tag: nextNotification.data?.component,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.triggeredNotifications.push(nextNotification);
|
||||||
|
|
||||||
|
this.observables.trigger.next(nextNotification);
|
||||||
|
|
||||||
|
notification.addEventListener('click', () => {
|
||||||
|
this.observables.click.next(nextNotification);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nextNotification.id) {
|
||||||
|
this.presentNotifications[nextNotification.id] = notification;
|
||||||
|
|
||||||
|
notification.addEventListener('close', () => {
|
||||||
|
delete(this.presentNotifications[nextNotification.id ?? 0]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scheduledNotifications.shift();
|
||||||
|
this.triggerNextNotification();
|
||||||
|
} else {
|
||||||
|
this.scheduleNotifications();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
update(options?: ILocalNotification): void {
|
||||||
|
if (!options?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = this.scheduledNotifications.findIndex((notification) => notification.id === options.id);
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.observables.update.next(options);
|
||||||
|
|
||||||
|
this.scheduledNotifications[index] = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async clear(notificationId: number | Array<number>): Promise<void> {
|
||||||
|
if (!Array.isArray(notificationId)) {
|
||||||
|
notificationId = [notificationId];
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationId.forEach((id) => {
|
||||||
|
if (!this.presentNotifications[id]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.presentNotifications[id].close();
|
||||||
|
|
||||||
|
this.observables.clear.next(this.presentNotifications[id]);
|
||||||
|
delete this.presentNotifications[id];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async clearAll(): Promise<void> {
|
||||||
|
for (const x in this.presentNotifications) {
|
||||||
|
this.presentNotifications[x].close();
|
||||||
|
}
|
||||||
|
this.presentNotifications = {};
|
||||||
|
|
||||||
|
this.observables.clearall.next();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async cancel(notificationId: number | Array<number>): Promise<void> {
|
||||||
|
if (!Array.isArray(notificationId)) {
|
||||||
|
notificationId = [notificationId];
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationId.forEach((id) => {
|
||||||
|
const index = this.scheduledNotifications.findIndex((notification) => notification.id === id);
|
||||||
|
this.observables.cancel.next(this.scheduledNotifications[index]);
|
||||||
|
|
||||||
|
this.scheduledNotifications.splice(index, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.scheduleNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async cancelAll(): Promise<void> {
|
||||||
|
window.clearTimeout(this.nextTimeout);
|
||||||
|
this.scheduledNotifications = [];
|
||||||
|
|
||||||
|
this.observables.cancelall.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async isPresent(notificationId: number): Promise<boolean> {
|
||||||
|
return !!this.presentNotifications[notificationId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async isScheduled(notificationId: number): Promise<boolean> {
|
||||||
|
return this.scheduledNotifications.some((notification) => notification.id === notificationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async isTriggered(notificationId: number): Promise<boolean> {
|
||||||
|
return this.triggeredNotifications.some((notification) => notification.id === notificationId);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async getIds(): Promise<Array<number>> {
|
||||||
|
const ids = await this.getScheduledIds();
|
||||||
|
const triggeredIds = await this.getTriggeredIds();
|
||||||
|
|
||||||
|
return Promise.resolve(ids.concat(triggeredIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async getTriggeredIds(): Promise<Array<number>> {
|
||||||
|
const ids = this.triggeredNotifications
|
||||||
|
.map((notification) => notification.id || 0)
|
||||||
|
.filter((id) => id > 0);
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async getScheduledIds(): Promise<Array<number>> {
|
||||||
|
const ids = this.scheduledNotifications
|
||||||
|
.map((notification) => notification.id || 0)
|
||||||
|
.filter((id) => id > 0);
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async get(notificationId: number): Promise<ILocalNotification> {
|
||||||
|
const notification = this.scheduledNotifications
|
||||||
|
.find((notification) => notification.id === notificationId);
|
||||||
|
|
||||||
|
if (!notification) {
|
||||||
|
throw new Error('Invalid Notification Id.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async getAll(): Promise<Array<ILocalNotification>> {
|
||||||
|
return this.scheduledNotifications.concat(this.triggeredNotifications);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async getAllScheduled(): Promise<Array<ILocalNotification>> {
|
||||||
|
return this.scheduledNotifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async getAllTriggered(): Promise<Array<ILocalNotification>> {
|
||||||
|
return this.triggeredNotifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async registerPermission(): Promise<boolean> {
|
||||||
|
// We need to ask the user for permission
|
||||||
|
const permission = await Notification.requestPermission();
|
||||||
|
|
||||||
|
this.hasGranted = permission === 'granted';
|
||||||
|
|
||||||
|
// If the user accepts, let's create a notification
|
||||||
|
return this.hasGranted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async hasPermission(): Promise<boolean> {
|
||||||
|
if (this.hasGranted !== undefined) {
|
||||||
|
return this.hasGranted;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('Notification' in window)) {
|
||||||
|
// Check if the browser supports notifications
|
||||||
|
throw new CoreError('This browser does not support desktop notification');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.registerPermission();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async addActions(groupId: unknown, actions: Array<ILocalNotificationAction>): Promise<Array<ILocalNotificationAction>> {
|
||||||
|
// Not implemented.
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async removeActions(groupId: unknown): Promise<unknown> {
|
||||||
|
// Not implemented.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async hasActions(groupId: unknown): Promise<boolean> {
|
||||||
|
// Not implemented.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async getDefaults(): Promise<unknown> {
|
||||||
|
// Not implemented.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async setDefaults(defaults: unknown): Promise<unknown> {
|
||||||
|
// Not implemented.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
on(eventName: string): Observable<unknown> {
|
||||||
|
if (!this.observables[eventName]) {
|
||||||
|
this.observables[eventName] = new Subject<ILocalNotification>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.observables[eventName];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
fireEvent(eventName: string, args: unknown): void {
|
||||||
|
if (!this.observables[eventName]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.observables[eventName].next(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async fireQueuedEvents(): Promise<unknown> {
|
||||||
|
return this.triggerNextNotification();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -27,9 +27,11 @@ import { CoreGradesModule } from './grades/grades.module';
|
||||||
import { CoreH5PModule } from './h5p/h5p.module';
|
import { CoreH5PModule } from './h5p/h5p.module';
|
||||||
import { CoreLoginModule } from './login/login.module';
|
import { CoreLoginModule } from './login/login.module';
|
||||||
import { CoreMainMenuModule } from './mainmenu/mainmenu.module';
|
import { CoreMainMenuModule } from './mainmenu/mainmenu.module';
|
||||||
|
import { CoreNativeModule } from '@features/native/native.module';
|
||||||
import { CorePushNotificationsModule } from './pushnotifications/pushnotifications.module';
|
import { CorePushNotificationsModule } from './pushnotifications/pushnotifications.module';
|
||||||
import { CoreQuestionModule } from './question/question.module';
|
import { CoreQuestionModule } from './question/question.module';
|
||||||
import { CoreRatingModule } from './rating/rating.module';
|
import { CoreRatingModule } from './rating/rating.module';
|
||||||
|
import { CoreRemindersModule } from './reminders/reminders.module';
|
||||||
import { CoreSearchModule } from './search/search.module';
|
import { CoreSearchModule } from './search/search.module';
|
||||||
import { CoreSettingsModule } from './settings/settings.module';
|
import { CoreSettingsModule } from './settings/settings.module';
|
||||||
import { CoreSharedFilesModule } from './sharedfiles/sharedfiles.module';
|
import { CoreSharedFilesModule } from './sharedfiles/sharedfiles.module';
|
||||||
|
@ -37,11 +39,10 @@ import { CoreSiteHomeModule } from './sitehome/sitehome.module';
|
||||||
import { CoreSitePluginsModule } from './siteplugins/siteplugins.module';
|
import { CoreSitePluginsModule } from './siteplugins/siteplugins.module';
|
||||||
import { CoreStylesModule } from './styles/styles.module';
|
import { CoreStylesModule } from './styles/styles.module';
|
||||||
import { CoreTagModule } from './tag/tag.module';
|
import { CoreTagModule } from './tag/tag.module';
|
||||||
import { CoreUserToursModule } from './usertours/user-tours.module';
|
|
||||||
import { CoreUserModule } from './user/user.module';
|
import { CoreUserModule } from './user/user.module';
|
||||||
|
import { CoreUserToursModule } from './usertours/user-tours.module';
|
||||||
import { CoreViewerModule } from './viewer/viewer.module';
|
import { CoreViewerModule } from './viewer/viewer.module';
|
||||||
import { CoreXAPIModule } from './xapi/xapi.module';
|
import { CoreXAPIModule } from './xapi/xapi.module';
|
||||||
import { CoreNativeModule } from '@features/native/native.module';
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -61,15 +62,16 @@ import { CoreNativeModule } from '@features/native/native.module';
|
||||||
CorePushNotificationsModule,
|
CorePushNotificationsModule,
|
||||||
CoreQuestionModule,
|
CoreQuestionModule,
|
||||||
CoreRatingModule,
|
CoreRatingModule,
|
||||||
|
CoreRemindersModule,
|
||||||
CoreSearchModule,
|
CoreSearchModule,
|
||||||
CoreSettingsModule,
|
CoreSettingsModule,
|
||||||
CoreSharedFilesModule,
|
CoreSharedFilesModule,
|
||||||
CoreSiteHomeModule,
|
CoreSiteHomeModule,
|
||||||
CoreSitePluginsModule,
|
CoreSitePluginsModule,
|
||||||
CoreTagModule,
|
|
||||||
CoreStylesModule,
|
CoreStylesModule,
|
||||||
CoreUserToursModule,
|
CoreTagModule,
|
||||||
CoreUserModule,
|
CoreUserModule,
|
||||||
|
CoreUserToursModule,
|
||||||
CoreViewerModule,
|
CoreViewerModule,
|
||||||
CoreXAPIModule,
|
CoreXAPIModule,
|
||||||
|
|
||||||
|
|
|
@ -473,11 +473,6 @@ export class CorePushNotificationsProvider {
|
||||||
return this.notificationClicked(data);
|
return this.notificationClicked(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the app is in foreground when the notification is received, it's not shown. Let's show it ourselves.
|
|
||||||
if (!CoreLocalNotifications.isAvailable()) {
|
|
||||||
return this.notifyReceived(notification, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
const localNotif: ILocalNotification = {
|
const localNotif: ILocalNotification = {
|
||||||
id: Number(data.notId) || 1,
|
id: Number(data.notId) || 1,
|
||||||
data: data,
|
data: data,
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
// (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 { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CoreRemindersDateComponent } from './date/date';
|
||||||
|
import { CoreRemindersSetButtonComponent } from './set-button/set-button';
|
||||||
|
import { CoreRemindersSetReminderCustomComponent } from './set-reminder-custom/set-reminder-custom';
|
||||||
|
import { CoreRemindersSetReminderMenuComponent } from './set-reminder-menu/set-reminder-menu';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
CoreRemindersDateComponent,
|
||||||
|
CoreRemindersSetButtonComponent,
|
||||||
|
CoreRemindersSetReminderCustomComponent,
|
||||||
|
CoreRemindersSetReminderMenuComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreSharedModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
CoreRemindersDateComponent,
|
||||||
|
CoreRemindersSetButtonComponent,
|
||||||
|
CoreRemindersSetReminderCustomComponent,
|
||||||
|
CoreRemindersSetReminderMenuComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreRemindersComponentsModule {}
|
|
@ -0,0 +1,8 @@
|
||||||
|
<div>
|
||||||
|
<ion-icon name="fas-calendar" aria-hidden="true"></ion-icon>
|
||||||
|
<strong>{{ label }}</strong> {{ readableTime }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<core-reminders-set-button *ngIf="showReminderButton" slot="end" [component]="component" [instanceId]="instanceId" [type]="type"
|
||||||
|
[label]="label" [timebefore]="timebefore" [time]="time" [title]="title" [url]="url">
|
||||||
|
</core-reminders-set-button>
|
|
@ -0,0 +1,21 @@
|
||||||
|
@import "~theme/globals";
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
@include margin-horizontal(0px, 4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
core-reminders-date + :host {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
// (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 { CoreReminders } from '@features/reminders/services/reminders';
|
||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
import { CoreTime } from '@singletons/time';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays a date to remind.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-reminders-date',
|
||||||
|
templateUrl: 'date.html',
|
||||||
|
styleUrls: ['date.scss'],
|
||||||
|
})
|
||||||
|
export class CoreRemindersDateComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input() component?: string;
|
||||||
|
@Input() instanceId?: number;
|
||||||
|
@Input() type?: string;
|
||||||
|
@Input() label = '';
|
||||||
|
@Input() time = 0;
|
||||||
|
@Input() relativeTo = 0;
|
||||||
|
@Input() title = '';
|
||||||
|
@Input() url = '';
|
||||||
|
|
||||||
|
showReminderButton = false;
|
||||||
|
timebefore?: number; // Undefined means no reminder has been set.
|
||||||
|
readableTime = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
this.readableTime = this.getReadableTime(this.time, this.relativeTo);
|
||||||
|
|
||||||
|
// If not set, button won't be shown.
|
||||||
|
if (this.component === undefined || this.instanceId === undefined || this.type === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remindersEnabled = CoreReminders.isEnabled();
|
||||||
|
this.showReminderButton = remindersEnabled && this.time > CoreTimeUtils.timestamp();
|
||||||
|
|
||||||
|
if (!this.showReminderButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reminders = await CoreReminders.getReminders({
|
||||||
|
instanceId: this.instanceId,
|
||||||
|
component: this.component,
|
||||||
|
type: this.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.timebefore = reminders[0]?.timebefore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the readable time.
|
||||||
|
*
|
||||||
|
* @param timestamp Timestamp.
|
||||||
|
* @param relativeTo Base timestamp if timestamp is relative to this one.
|
||||||
|
* @return Readable time string.
|
||||||
|
*/
|
||||||
|
protected getReadableTime(timestamp: number, relativeTo = 0): string {
|
||||||
|
if (!relativeTo) {
|
||||||
|
return CoreTimeUtils.userDate(timestamp * 1000, 'core.strftimedatetime', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Translate.instant(
|
||||||
|
'core.course.relativedatessubmissionduedate' + (timestamp > relativeTo ? 'after' : 'before'),
|
||||||
|
{
|
||||||
|
$a: {
|
||||||
|
datediffstr: relativeTo === timestamp ?
|
||||||
|
'0 ' + Translate.instant('core.secs') :
|
||||||
|
CoreTime.formatTime(relativeTo - timestamp, 3),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
<ion-button fill="clear" size="small" (click)="setReminder($event)"
|
||||||
|
[attr.aria-label]="'core.reminders.setareminderfor' | translate : { title: title, label: labelClean }"
|
||||||
|
[attr.aria-checked]="timebefore !== undefined">
|
||||||
|
<ion-icon name="fas-bell" slot="icon-only" *ngIf="timebefore !== undefined" aria-hidden="true"></ion-icon>
|
||||||
|
<ion-icon name="far-bell-slash" slot="icon-only" *ngIf="timebefore === undefined" aria-hidden="true"></ion-icon>
|
||||||
|
</ion-button>
|
|
@ -0,0 +1,134 @@
|
||||||
|
// (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 { CoreReminderData, CoreReminders, CoreRemindersService } from '@features/reminders/services/reminders';
|
||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreRemindersSetReminderMenuComponent } from '../set-reminder-menu/set-reminder-menu';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays a button to set a reminder.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-reminders-set-button',
|
||||||
|
templateUrl: 'set-button.html',
|
||||||
|
})
|
||||||
|
export class CoreRemindersSetButtonComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input() component?: string;
|
||||||
|
@Input() instanceId?: number;
|
||||||
|
@Input() type?: string;
|
||||||
|
@Input() label = '';
|
||||||
|
@Input() timebefore?: number;
|
||||||
|
@Input() time = -1;
|
||||||
|
@Input() title = '';
|
||||||
|
@Input() url = '';
|
||||||
|
|
||||||
|
labelClean = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.labelClean = this.label.replace(':', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set reminder.
|
||||||
|
*
|
||||||
|
* @param ev Click event.
|
||||||
|
*/
|
||||||
|
async setReminder(ev: Event): Promise<void> {
|
||||||
|
if (this.component === undefined || this.instanceId === undefined || this.type === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
if (this.timebefore === undefined) {
|
||||||
|
// Set it to the time of the event.
|
||||||
|
this.saveReminder(0);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open popover.
|
||||||
|
const reminderTime = await CoreDomUtils.openPopover<{timeBefore: number}>({
|
||||||
|
component: CoreRemindersSetReminderMenuComponent,
|
||||||
|
componentProps: {
|
||||||
|
initialValue: this.timebefore,
|
||||||
|
eventTime: this.time,
|
||||||
|
noReminderLabel: 'core.reminders.delete',
|
||||||
|
},
|
||||||
|
event: ev,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (reminderTime === undefined) {
|
||||||
|
// User canceled.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save before.
|
||||||
|
this.saveReminder(reminderTime.timeBefore);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save reminder.
|
||||||
|
*
|
||||||
|
* @param timebefore Time before the event to fire the notification.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async saveReminder(timebefore: number): Promise<void> {
|
||||||
|
if (this.component === undefined || this.instanceId === undefined || this.type === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timebefore === undefined || timebefore === CoreRemindersService.DISABLED) {
|
||||||
|
// Remove the reminder.
|
||||||
|
await CoreReminders.removeReminders({
|
||||||
|
instanceId: this.instanceId,
|
||||||
|
component: this.component,
|
||||||
|
type: this.type,
|
||||||
|
});
|
||||||
|
this.timebefore = undefined;
|
||||||
|
|
||||||
|
CoreDomUtils.showToast('core.reminders.reminderunset', true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timebefore = timebefore;
|
||||||
|
|
||||||
|
const reminder: CoreReminderData = {
|
||||||
|
component: this.component,
|
||||||
|
instanceId: this.instanceId,
|
||||||
|
timebefore: this.timebefore,
|
||||||
|
type: this.type,
|
||||||
|
title: this.label + ' ' + this.title,
|
||||||
|
url: this.url,
|
||||||
|
time: this.time,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save before.
|
||||||
|
await CoreReminders.addReminder(reminder);
|
||||||
|
|
||||||
|
const time = this.time - timebefore;
|
||||||
|
const text = Translate.instant('core.reminders.reminderset', { $a: CoreTimeUtils.userDate(time * 1000) });
|
||||||
|
CoreDomUtils.showToast(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>
|
||||||
|
<h2>{{ 'core.reminders.customreminder' | translate }}</h2>
|
||||||
|
</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-item class="ion-text-wrap">
|
||||||
|
<div class="flex-row">
|
||||||
|
<!-- Input to enter the value. -->
|
||||||
|
<ion-input id="reminderValue" type="number" name="customvalue" [(ngModel)]="customValue" placeholder="10"
|
||||||
|
[attr.aria-label]="'core.reminders.value' | translate">
|
||||||
|
</ion-input>
|
||||||
|
|
||||||
|
<!-- Units. -->
|
||||||
|
<span class="accesshide" id="reminderUnits_label">{{ 'core.reminders.units' | translate }}</span>
|
||||||
|
<ion-select aria-labelledby="reminderUnits_label" name="customunits" [(ngModel)]="customUnits" interface="action-sheet"
|
||||||
|
slot="end" [interfaceOptions]="{header: 'core.reminders.units' | translate}">
|
||||||
|
<ion-select-option *ngFor=" let option of customUnitsOptions" [value]="option.value">
|
||||||
|
{{ option.label | translate }}
|
||||||
|
</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
</div>
|
||||||
|
</ion-item>
|
||||||
|
<ion-grid>
|
||||||
|
<ion-row>
|
||||||
|
<ion-col>
|
||||||
|
<ion-button expand="block" fill="outline" (click)="cancel()">{{ 'core.cancel' | translate }}</ion-button>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col>
|
||||||
|
<ion-button expand="block" (click)="set()">
|
||||||
|
{{ 'core.reminders.setreminder' | translate }}
|
||||||
|
</ion-button>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,64 @@
|
||||||
|
// (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 { Component, Input } from '@angular/core';
|
||||||
|
import { CoreRemindersUnits } from '@features/reminders/services/reminders';
|
||||||
|
import { PopoverController } from '@singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component is meant to set a custom reminder
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
templateUrl: 'set-reminder-custom.html',
|
||||||
|
})
|
||||||
|
export class CoreRemindersSetReminderCustomComponent {
|
||||||
|
|
||||||
|
@Input() customValue = 10;
|
||||||
|
@Input() customUnits = CoreRemindersUnits.MINUTE;
|
||||||
|
|
||||||
|
customUnitsOptions = [
|
||||||
|
{
|
||||||
|
value: CoreRemindersUnits.MINUTE,
|
||||||
|
label: 'core.minutes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: CoreRemindersUnits.HOUR,
|
||||||
|
label: 'core.hours',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: CoreRemindersUnits.DAY,
|
||||||
|
label: 'core.days',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: CoreRemindersUnits.WEEK,
|
||||||
|
label: 'core.weeks',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set custom reminder.
|
||||||
|
*/
|
||||||
|
set(): void {
|
||||||
|
// Return it as an object because 0 means undefined if not.
|
||||||
|
PopoverController.dismiss({ value: this.customValue, unit: this.customUnits });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close popup.
|
||||||
|
*/
|
||||||
|
cancel(): void {
|
||||||
|
PopoverController.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>
|
||||||
|
<h2>{{ 'core.reminders.setareminder' | translate }}</h2>
|
||||||
|
</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-content>
|
||||||
|
<!-- Preset options. -->
|
||||||
|
<ng-container *ngFor="let option of presetOptions">
|
||||||
|
<ion-item button class="ion-text-wrap" (click)="setReminder(option.radioValue)" detail="false" *ngIf="option.enabled"
|
||||||
|
[attr.aria-selected]="currentValue === option.radioValue">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ option.label }}</p>
|
||||||
|
</ion-label>
|
||||||
|
<ion-icon name="fas-check" *ngIf="currentValue === option.radioValue" slot="end" aria-hidden="true"></ion-icon>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Custom value. -->
|
||||||
|
<ion-item button class="ion-text-wrap" (click)="setCustom($event)" detail="false" [attr.aria-selected]="currentValue === 'custom'">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ 'core.reminders.custom' | translate }}</p>
|
||||||
|
<p>{{ customLabel }}</p>
|
||||||
|
</ion-label>
|
||||||
|
<ion-icon name="fas-check" *ngIf="currentValue === 'custom'" slot="end" aria-hidden="true"></ion-icon>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item *ngIf="noReminderLabel" button class="ion-text-wrap text-danger border-top" (click)="disableReminder()" detail="false"
|
||||||
|
[attr.aria-selected]="currentValue === 'disabled'">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ noReminderLabel | translate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
<ion-icon name="fas-check" *ngIf="currentValue === 'disabled'" slot="end" aria-hidden="true"></ion-icon>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,3 @@
|
||||||
|
ion-item.border-top {
|
||||||
|
border-top: 1px solid var(--stroke);
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
// (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 { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import {
|
||||||
|
CoreReminders,
|
||||||
|
CoreRemindersService,
|
||||||
|
CoreRemindersUnits,
|
||||||
|
CoreReminderValueAndUnit,
|
||||||
|
} from '@features/reminders/services/reminders';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { PopoverController } from '@singletons';
|
||||||
|
import { CoreRemindersSetReminderCustomComponent } from '../set-reminder-custom/set-reminder-custom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component is meant to display a popover with the reminder options.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
templateUrl: 'set-reminder-menu.html',
|
||||||
|
styleUrls: ['set-reminder-menu.scss'],
|
||||||
|
})
|
||||||
|
export class CoreRemindersSetReminderMenuComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input() initialValue?: number;
|
||||||
|
@Input() eventTime?: number;
|
||||||
|
@Input() noReminderLabel = '';
|
||||||
|
|
||||||
|
currentValue = '0m';
|
||||||
|
customLabel = '';
|
||||||
|
|
||||||
|
protected customValue = 10;
|
||||||
|
protected customUnits = CoreRemindersUnits.MINUTE;
|
||||||
|
|
||||||
|
presetOptions = [
|
||||||
|
{
|
||||||
|
radioValue: '0m',
|
||||||
|
value: 0,
|
||||||
|
unit: CoreRemindersUnits.MINUTE,
|
||||||
|
label: '',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
radioValue: '1h',
|
||||||
|
value: 1,
|
||||||
|
unit: CoreRemindersUnits.HOUR,
|
||||||
|
label: '',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
radioValue: '12h',
|
||||||
|
value: 12,
|
||||||
|
unit: CoreRemindersUnits.HOUR,
|
||||||
|
label: '',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
radioValue: '1d',
|
||||||
|
value: 1,
|
||||||
|
unit: CoreRemindersUnits.DAY,
|
||||||
|
label: '',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
this.presetOptions.forEach((option) => {
|
||||||
|
option.label = CoreReminders.getUnitValueLabel(option.value, option.unit);
|
||||||
|
option.enabled = this.isValidTime(option.unit, option.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialValue = CoreRemindersService.convertSecondsToValueAndUnit(this.initialValue);
|
||||||
|
if (initialValue.value === CoreRemindersService.DISABLED) {
|
||||||
|
this.currentValue = 'disabled';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search if it's one of the preset options.
|
||||||
|
const option = this.presetOptions.find(option =>
|
||||||
|
option.value === initialValue?.value && option.unit === initialValue.unit);
|
||||||
|
|
||||||
|
if (option) {
|
||||||
|
this.currentValue = option.radioValue;
|
||||||
|
} else {
|
||||||
|
// It's a custom value.
|
||||||
|
this.currentValue = 'custom';
|
||||||
|
this.customValue = initialValue.value;
|
||||||
|
this.customUnits = initialValue.unit;
|
||||||
|
this.customLabel = CoreReminders.getUnitValueLabel(this.customValue, this.customUnits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the reminder.
|
||||||
|
*
|
||||||
|
* @param value Value to set.
|
||||||
|
*/
|
||||||
|
setReminder(value?: string): void {
|
||||||
|
const option = this.presetOptions.find(option => option.radioValue === value);
|
||||||
|
|
||||||
|
if (!option) {
|
||||||
|
PopoverController.dismiss();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PopoverController.dismiss({ timeBefore: option.unit * option.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable the reminder.
|
||||||
|
*/
|
||||||
|
disableReminder(): void {
|
||||||
|
PopoverController.dismiss({ timeBefore: undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the time is on the future.
|
||||||
|
*
|
||||||
|
* @param unit Time unit.
|
||||||
|
* @param value Time value.
|
||||||
|
* @return Wether is a valid time or not.
|
||||||
|
*/
|
||||||
|
protected isValidTime(unit: number, value: number): boolean {
|
||||||
|
if (!this.eventTime) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timebefore = unit * value;
|
||||||
|
|
||||||
|
return (this.eventTime - timebefore) * 1000 > Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom value input clicked.
|
||||||
|
*
|
||||||
|
* @param ev Click event.
|
||||||
|
*/
|
||||||
|
async setCustom(ev: Event): Promise<void> {
|
||||||
|
const reminderTime = await CoreDomUtils.openPopover<CoreReminderValueAndUnit>({
|
||||||
|
component: CoreRemindersSetReminderCustomComponent,
|
||||||
|
componentProps: {
|
||||||
|
customValue: this.customValue,
|
||||||
|
customUnits: this.customUnits,
|
||||||
|
},
|
||||||
|
waitForDismissCompleted: true, // To be able to close parent popup.
|
||||||
|
event: ev,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (reminderTime === undefined) {
|
||||||
|
// User canceled.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentValue = 'custom';
|
||||||
|
this.customValue = reminderTime.value;
|
||||||
|
this.customUnits = reminderTime.unit;
|
||||||
|
this.customLabel = CoreReminders.getUnitValueLabel(this.customValue, this.customUnits);
|
||||||
|
|
||||||
|
// Let the dimissed popover to be removed.
|
||||||
|
await CoreUtils.nextTick();
|
||||||
|
|
||||||
|
PopoverController.dismiss({ timeBefore: Math.abs(this.customValue) * this.customUnits });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"atthetime": "At the time of the event",
|
||||||
|
"custom": "Custom...",
|
||||||
|
"customreminder": "Custom reminder",
|
||||||
|
"delete": "Delete reminder",
|
||||||
|
"reminderset": "Reminder set for {{$a}}",
|
||||||
|
"reminderunset": "Reminder deleted",
|
||||||
|
"setareminder": "Set a reminder",
|
||||||
|
"setareminderfor": "Set a reminder for \"{{title}}\" ({{label}})",
|
||||||
|
"setreminder": "Set reminder",
|
||||||
|
"timebefore": "{{value}} {{units}} before",
|
||||||
|
"units": "Units",
|
||||||
|
"value": "Value"
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { APP_INITIALIZER, NgModule, Type } from '@angular/core';
|
||||||
|
import { CORE_SITE_SCHEMAS } from '@services/sites';
|
||||||
|
import { CoreRemindersComponentsModule } from './components/components.module';
|
||||||
|
import { REMINDERS_SITE_SCHEMA } from './services/database/reminders';
|
||||||
|
import { CoreReminders, CoreRemindersService } from './services/reminders';
|
||||||
|
|
||||||
|
export const CORE_REMINDERS_SERVICES: Type<unknown>[] = [
|
||||||
|
CoreRemindersService,
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CoreRemindersComponentsModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: CORE_SITE_SCHEMAS,
|
||||||
|
useValue: [REMINDERS_SITE_SCHEMA],
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
multi: true,
|
||||||
|
useValue: async () => {
|
||||||
|
await CoreReminders.initialize();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreRemindersModule {}
|
|
@ -0,0 +1,225 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { AddonCalendarProvider } from '@addons/calendar/services/calendar';
|
||||||
|
import { AddonCalendarEventDBRecord, EVENTS_TABLE } from '@addons/calendar/services/database/calendar';
|
||||||
|
import { SQLiteDB } from '@classes/sqlitedb';
|
||||||
|
import { CoreSiteSchema } from '@services/sites';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreReminderData, CoreRemindersService } from '../reminders';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database variables for CoreRemindersService service.
|
||||||
|
*/
|
||||||
|
export const REMINDERS_TABLE = 'core_reminders';
|
||||||
|
export const REMINDERS_SITE_SCHEMA: CoreSiteSchema = {
|
||||||
|
name: 'CoreRemindersService',
|
||||||
|
version: 1,
|
||||||
|
canBeCleared: [],
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: REMINDERS_TABLE,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'INTEGER',
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'component',
|
||||||
|
type: 'TEXT',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'instanceId',
|
||||||
|
type: 'INTEGER',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'TEXT',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'time',
|
||||||
|
type: 'INTEGER',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timebefore',
|
||||||
|
type: 'INTEGER',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'TEXT',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'url',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
|
||||||
|
],
|
||||||
|
uniqueKeys: [
|
||||||
|
['component', 'instanceId', 'timebefore'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
install: async (db: SQLiteDB): Promise<void> => {
|
||||||
|
await migrateFromCalendarRemindersV1(db);
|
||||||
|
await migrateFromCalendarRemindersV2(db);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrateFromCalendarRemindersV1 = async (db: SQLiteDB): Promise<void> => {
|
||||||
|
// Migrate reminders. New format @since 4.0.
|
||||||
|
const oldTable = 'addon_calendar_reminders';
|
||||||
|
|
||||||
|
const tableExists = await CoreUtils.promiseWorks(db.tableExists(oldTable));
|
||||||
|
if (!tableExists) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const records = await db.getAllRecords<AddonCalendarReminderDBRecord>(oldTable);
|
||||||
|
const events: Record<number, AddonCalendarEventDBRecord> = {};
|
||||||
|
const uniqueReminder: Record<number, number[]> = {};
|
||||||
|
|
||||||
|
await Promise.all(records.map(async (record) => {
|
||||||
|
// Get the event to compare the reminder time with the event time.
|
||||||
|
if (!events[record.eventid]) {
|
||||||
|
try {
|
||||||
|
events[record.eventid] = await db.getRecord(EVENTS_TABLE, { id: record.eventid });
|
||||||
|
} catch {
|
||||||
|
// Event not found in local DB, shouldn't happen. Ignore the reminder.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = events[record.eventid];
|
||||||
|
|
||||||
|
let reminderTime = record.time;
|
||||||
|
|
||||||
|
if (!reminderTime || reminderTime === -1) {
|
||||||
|
// Default reminder.
|
||||||
|
reminderTime = CoreRemindersService.DEFAULT_REMINDER_TIMEBEFORE;
|
||||||
|
} else if (reminderTime > event.timestart) {
|
||||||
|
// Reminder is after the event, ignore it.
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Remove seconds from the old reminder, it could include seconds by mistake.
|
||||||
|
reminderTime = event.timestart - Math.floor(reminderTime / 60) * 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof uniqueReminder[record.eventid] === undefined) {
|
||||||
|
uniqueReminder[record.eventid] = [];
|
||||||
|
} else {
|
||||||
|
if (uniqueReminder[record.eventid].includes(reminderTime)) {
|
||||||
|
// Reminder already exists.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await createReminder(db, event, reminderTime);
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.dropTable(oldTable);
|
||||||
|
} catch {
|
||||||
|
// Error deleting old table, ignore.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrateFromCalendarRemindersV2 = async (db: SQLiteDB): Promise<void> => {
|
||||||
|
const oldTable = 'addon_calendar_reminders_2';
|
||||||
|
|
||||||
|
const tableExists = await CoreUtils.promiseWorks(db.tableExists(oldTable));
|
||||||
|
if (!tableExists) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const records = await db.getAllRecords<AddonCalendarReminderDBRecord>(oldTable);
|
||||||
|
const events: Record<number, AddonCalendarEventDBRecord> = {};
|
||||||
|
const uniqueReminder: Record<number, number[]> = {};
|
||||||
|
|
||||||
|
await Promise.all(records.map(async (record) => {
|
||||||
|
// Get the event to compare the reminder time with the event time.
|
||||||
|
if (!events[record.eventid]) {
|
||||||
|
try {
|
||||||
|
events[record.eventid] = await db.getRecord(EVENTS_TABLE, { id: record.eventid });
|
||||||
|
} catch {
|
||||||
|
// Event not found in local DB, shouldn't happen. Ignore the reminder.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const event = events[record.eventid];
|
||||||
|
|
||||||
|
const reminderTime = record.time || CoreRemindersService.DEFAULT_REMINDER_TIMEBEFORE;
|
||||||
|
|
||||||
|
if (typeof uniqueReminder[record.eventid] === undefined) {
|
||||||
|
uniqueReminder[record.eventid] = [];
|
||||||
|
} else {
|
||||||
|
if (uniqueReminder[record.eventid].includes(reminderTime)) {
|
||||||
|
// Reminder already exists.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqueReminder[record.eventid].push(reminderTime);
|
||||||
|
|
||||||
|
await createReminder(db, event, reminderTime);
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.dropTable(oldTable);
|
||||||
|
} catch {
|
||||||
|
// Error deleting old table, ignore.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createReminder = async (
|
||||||
|
db: SQLiteDB,
|
||||||
|
event: AddonCalendarEventDBRecord,
|
||||||
|
reminderTime: number,
|
||||||
|
): Promise<void> => {
|
||||||
|
const reminder: CoreReminderData = {
|
||||||
|
component: AddonCalendarProvider.COMPONENT,
|
||||||
|
instanceId: event.id,
|
||||||
|
type: event.eventtype,
|
||||||
|
timebefore: reminderTime,
|
||||||
|
url: event.url,
|
||||||
|
title: event.name,
|
||||||
|
time: event.timestart,
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.insertRecord(REMINDERS_TABLE, reminder);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CoreReminderDBRecord = {
|
||||||
|
id: number; // Reminder ID.
|
||||||
|
component: string; // Component where the reminder belongs.
|
||||||
|
instanceId: number; // Instance Id where the reminder belongs.
|
||||||
|
type: string; // Event idenfier type.
|
||||||
|
time: number; // Event time.
|
||||||
|
timebefore: number; // Seconds before the event to remind.
|
||||||
|
title: string; // Notification title.
|
||||||
|
url?: string; // URL where to redirect the user.
|
||||||
|
};
|
||||||
|
|
||||||
|
type AddonCalendarReminderDBRecord = {
|
||||||
|
id: number;
|
||||||
|
eventid: number;
|
||||||
|
time: number | null; // Number of seconds before the event, null for default time.
|
||||||
|
timecreated?: number | null;
|
||||||
|
};
|
|
@ -0,0 +1,468 @@
|
||||||
|
// (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 { Injectable } from '@angular/core';
|
||||||
|
import { CoreLocalNotifications } from '@services/local-notifications';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
|
import { CoreReminderDBRecord, REMINDERS_TABLE } from './database/reminders';
|
||||||
|
import { ILocalNotification } from '@ionic-native/local-notifications';
|
||||||
|
import { CorePlatform } from '@services/platform';
|
||||||
|
import { CoreConstants } from '@/core/constants';
|
||||||
|
import { CoreConfig } from '@services/config';
|
||||||
|
import { CoreEvents } from '@singletons/events';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Units to set a reminder.
|
||||||
|
*/
|
||||||
|
export enum CoreRemindersUnits {
|
||||||
|
MINUTE = CoreConstants.SECONDS_MINUTE,
|
||||||
|
HOUR = CoreConstants.SECONDS_HOUR,
|
||||||
|
DAY = CoreConstants.SECONDS_DAY,
|
||||||
|
WEEK = CoreConstants.SECONDS_WEEK,
|
||||||
|
}
|
||||||
|
|
||||||
|
const REMINDER_UNITS_LABELS = {
|
||||||
|
single: {
|
||||||
|
[CoreRemindersUnits.MINUTE]: 'core.minute',
|
||||||
|
[CoreRemindersUnits.HOUR]: 'core.hour',
|
||||||
|
[CoreRemindersUnits.DAY]: 'core.day',
|
||||||
|
[CoreRemindersUnits.WEEK]: 'core.week',
|
||||||
|
},
|
||||||
|
multi: {
|
||||||
|
[CoreRemindersUnits.MINUTE]: 'core.minutes',
|
||||||
|
[CoreRemindersUnits.HOUR]: 'core.hours',
|
||||||
|
[CoreRemindersUnits.DAY]: 'core.days',
|
||||||
|
[CoreRemindersUnits.WEEK]: 'core.weeks',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle reminders.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreRemindersService {
|
||||||
|
|
||||||
|
static readonly DEFAULT_REMINDER_TIMEBEFORE = -1;
|
||||||
|
static readonly DISABLED = -1;
|
||||||
|
|
||||||
|
static readonly DEFAULT_NOTIFICATION_TIME_SETTING = 'CoreRemindersDefaultNotification';
|
||||||
|
static readonly DEFAULT_NOTIFICATION_TIME_CHANGED = 'CoreRemindersDefaultNotificationChangedEvent';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the service.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
if (!this.isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scheduleAllNotifications();
|
||||||
|
|
||||||
|
CoreEvents.on(CoreRemindersService.DEFAULT_NOTIFICATION_TIME_CHANGED, async (data) => {
|
||||||
|
const site = await CoreSites.getSite(data.siteId);
|
||||||
|
const siteId = site.getId();
|
||||||
|
|
||||||
|
// Get all the events that have a default reminder.
|
||||||
|
const reminders = await this.getRemindersWithDefaultTime(siteId);
|
||||||
|
|
||||||
|
// Reschedule all the default reminders.
|
||||||
|
reminders.forEach((reminder) =>
|
||||||
|
this.scheduleNotification(reminder, siteId));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns if Reminders are enabled.
|
||||||
|
*
|
||||||
|
* @return True if reminders are enabled and available, false otherwise.
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save reminder to Database.
|
||||||
|
*
|
||||||
|
* @param reminder Reminder to set.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Resolved when done. Rejected on failure.
|
||||||
|
*/
|
||||||
|
async addReminder(reminder: CoreReminderData, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
const reminderId = await site.getDb().insertRecord(REMINDERS_TABLE, reminder);
|
||||||
|
|
||||||
|
const reminderRecord: CoreReminderDBRecord = Object.assign(reminder, { id: reminderId });
|
||||||
|
|
||||||
|
await this.scheduleNotification(reminderRecord, site.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a reminder from local Db.
|
||||||
|
*
|
||||||
|
* @param reminder Fields to update.
|
||||||
|
* @param siteId ID of the site the reminder belongs to. If not defined, use current site.
|
||||||
|
* @return Promise resolved when the reminder data is updated.
|
||||||
|
*/
|
||||||
|
async updateReminder(
|
||||||
|
reminder: CoreReminderDBRecord,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
await site.getDb().updateRecords(REMINDERS_TABLE, reminder, { id: reminder.id });
|
||||||
|
|
||||||
|
// Reschedule.
|
||||||
|
await this.scheduleNotification(reminder, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update all reminders of a component and instance from local Db.
|
||||||
|
*
|
||||||
|
* @param newFields Fields to update.
|
||||||
|
* @param selector Reminder selector.
|
||||||
|
* @param siteId ID of the site the reminder belongs to. If not defined, use current site.
|
||||||
|
* @return Promise resolved when the reminder data is updated.
|
||||||
|
*/
|
||||||
|
async updateReminders(
|
||||||
|
newFields: Partial<CoreReminderData>,
|
||||||
|
selector: CoreReminderSelector,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
const reminders = await this.getReminders(selector, site.getId());
|
||||||
|
|
||||||
|
await Promise.all(reminders.map((reminder) => {
|
||||||
|
reminder = Object.assign(reminder, newFields);
|
||||||
|
|
||||||
|
return this.updateReminder(reminder, site.getId());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all reminders from local Db.
|
||||||
|
*
|
||||||
|
* @param siteId ID of the site the reminder belongs to. If not defined, use current site.
|
||||||
|
* @return Promise resolved when the reminder data is retrieved.
|
||||||
|
*/
|
||||||
|
async getAllReminders(siteId?: string): Promise<CoreReminderDBRecord[]> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
return site.getDb().getRecords(REMINDERS_TABLE, undefined, 'time ASC');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all reminders of a component and instance from local Db.
|
||||||
|
*
|
||||||
|
* @param selector Reminder selector.
|
||||||
|
* @param siteId ID of the site the reminder belongs to. If not defined, use current site.
|
||||||
|
* @return Promise resolved when the reminder data is retrieved.
|
||||||
|
*/
|
||||||
|
async getReminders(selector: CoreReminderSelector, siteId?: string): Promise<CoreReminderDBRecord[]> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
return site.getDb().getRecords(REMINDERS_TABLE, selector, 'time ASC');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all reminders of a component with default time.
|
||||||
|
*
|
||||||
|
* @param siteId ID of the site the reminder belongs to. If not defined, use current site.
|
||||||
|
* @return Promise resolved when the reminder data is retrieved.
|
||||||
|
*/
|
||||||
|
protected async getRemindersWithDefaultTime(siteId?: string): Promise<CoreReminderDBRecord[]> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
return site.getDb().getRecords<CoreReminderDBRecord>(
|
||||||
|
REMINDERS_TABLE,
|
||||||
|
{ timebefore: CoreRemindersService.DEFAULT_REMINDER_TIMEBEFORE },
|
||||||
|
'time ASC',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a reminder and cancel the notification.
|
||||||
|
*
|
||||||
|
* @param id Reminder ID.
|
||||||
|
* @param siteId ID of the site the reminder belongs to. If not defined, use current site.
|
||||||
|
* @return Promise resolved when the notification is updated.
|
||||||
|
*/
|
||||||
|
async removeReminder(id: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
const reminder = await site.getDb().getRecord<CoreReminderDBRecord>(REMINDERS_TABLE, { id });
|
||||||
|
|
||||||
|
if (this.isEnabled()) {
|
||||||
|
this.cancelReminder(id, reminder.component, site.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
await site.getDb().deleteRecords(REMINDERS_TABLE, { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all reminders of the same element.
|
||||||
|
*
|
||||||
|
* @param selector Reminder selector.
|
||||||
|
* @param siteId ID of the site the reminder belongs to. If not defined, use current site.
|
||||||
|
* @return Promise resolved when the notification is updated.
|
||||||
|
*/
|
||||||
|
async removeReminders(selector: CoreReminderSelector, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
siteId = site.getId();
|
||||||
|
|
||||||
|
if (this.isEnabled()) {
|
||||||
|
const reminders = await this.getReminders(selector, siteId);
|
||||||
|
|
||||||
|
reminders.forEach((reminder) => {
|
||||||
|
this.cancelReminder(reminder.id, reminder.component, siteId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await site.getDb().deleteRecords(REMINDERS_TABLE, selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel a notification for a reminder.
|
||||||
|
*
|
||||||
|
* @param reminderId Reminder Id to cancel.
|
||||||
|
* @param component Reminder component.
|
||||||
|
* @param siteId ID of the site the reminder belongs to. If not defined, use current site.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async cancelReminder(reminderId: number, component: string, siteId?: string): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
|
return CoreLocalNotifications.cancel(reminderId, component, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a notification. If local notification plugin is not enabled, resolve the promise.
|
||||||
|
*
|
||||||
|
* @param reminder Reminder to schedule.
|
||||||
|
* @param siteId Site ID the reminder belongs to. If not defined, use current site.
|
||||||
|
* @return Promise resolved when the notification is scheduled.
|
||||||
|
*/
|
||||||
|
async scheduleNotification(
|
||||||
|
reminder: CoreReminderDBRecord,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
|
||||||
|
if (!this.isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
|
const timebefore = reminder.timebefore === CoreRemindersService.DEFAULT_REMINDER_TIMEBEFORE
|
||||||
|
? await this.getDefaultNotificationTime(siteId)
|
||||||
|
: reminder.timebefore;
|
||||||
|
|
||||||
|
if (timebefore === CoreRemindersService.DISABLED) {
|
||||||
|
// Notification disabled. Cancel.
|
||||||
|
return this.cancelReminder(reminder.id, reminder.component, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const notificationTime = (reminder.time - timebefore) * 1000;
|
||||||
|
|
||||||
|
if (notificationTime <= Date.now()) { // @TODO Add a threshold.
|
||||||
|
// This reminder is over, don't schedule. Cancel if it was scheduled.
|
||||||
|
return this.cancelReminder(reminder.id, reminder.component, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const notificationData: CoreRemindersPushNotificationData = {
|
||||||
|
reminderId: reminder.id,
|
||||||
|
instanceId: reminder.instanceId,
|
||||||
|
siteId: siteId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const notification: ILocalNotification = {
|
||||||
|
id: reminder.id,
|
||||||
|
title: reminder.title,
|
||||||
|
text: CoreTimeUtils.userDate(reminder.time * 1000, 'core.strftimedaydatetime', true),
|
||||||
|
icon: 'file://assets/img/icons/calendar.png',
|
||||||
|
trigger: {
|
||||||
|
at: new Date(notificationTime),
|
||||||
|
},
|
||||||
|
data: notificationData,
|
||||||
|
};
|
||||||
|
|
||||||
|
return CoreLocalNotifications.schedule(notification, reminder.component, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the all saved reminders and schedule the notification.
|
||||||
|
* If local notification plugin is not enabled, resolve the promise.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when all the notifications have been scheduled.
|
||||||
|
*/
|
||||||
|
async scheduleAllNotifications(): Promise<void> {
|
||||||
|
await CorePlatform.ready();
|
||||||
|
|
||||||
|
if (CoreLocalNotifications.isPluginAvailable()) {
|
||||||
|
// Notifications are already scheduled.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const siteIds = await CoreSites.getSitesIds();
|
||||||
|
|
||||||
|
await Promise.all(siteIds.map((siteId: string) => async () => {
|
||||||
|
const reminders = await this.getAllReminders(siteId);
|
||||||
|
|
||||||
|
reminders.forEach((reminder) => {
|
||||||
|
this.scheduleNotification(reminder, siteId);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: CoreRemindersUnits, addDefaultLabel = false): string {
|
||||||
|
if (value === CoreRemindersService.DISABLED) {
|
||||||
|
return Translate.instant('core.settings.disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === 0) {
|
||||||
|
return Translate.instant('core.reminders.atthetime');
|
||||||
|
}
|
||||||
|
|
||||||
|
const unitsLabel = value === 1 ?
|
||||||
|
REMINDER_UNITS_LABELS.single[unit] :
|
||||||
|
REMINDER_UNITS_LABELS.multi[unit];
|
||||||
|
|
||||||
|
const label = Translate.instant('core.reminders.timebefore', {
|
||||||
|
units: Translate.instant(unitsLabel),
|
||||||
|
value: value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (addDefaultLabel) {
|
||||||
|
return Translate.instant('core.defaultvalue', { $a: label });
|
||||||
|
}
|
||||||
|
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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): CoreReminderValueAndUnit {
|
||||||
|
if (seconds === undefined || seconds < 0) {
|
||||||
|
return {
|
||||||
|
value: CoreRemindersService.DISABLED,
|
||||||
|
unit: CoreRemindersUnits.MINUTE,
|
||||||
|
};
|
||||||
|
} else if (seconds === 0) {
|
||||||
|
return {
|
||||||
|
value: 0,
|
||||||
|
unit: CoreRemindersUnits.MINUTE,
|
||||||
|
};
|
||||||
|
} else if (seconds % CoreRemindersUnits.WEEK === 0) {
|
||||||
|
return {
|
||||||
|
value: seconds / CoreRemindersUnits.WEEK,
|
||||||
|
unit: CoreRemindersUnits.WEEK,
|
||||||
|
};
|
||||||
|
} else if (seconds % CoreRemindersUnits.DAY === 0) {
|
||||||
|
return {
|
||||||
|
value: seconds / CoreRemindersUnits.DAY,
|
||||||
|
unit: CoreRemindersUnits.DAY,
|
||||||
|
};
|
||||||
|
} else if (seconds % CoreRemindersUnits.HOUR === 0) {
|
||||||
|
return {
|
||||||
|
value: seconds / CoreRemindersUnits.HOUR,
|
||||||
|
unit: CoreRemindersUnits.HOUR,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
value: seconds / CoreRemindersUnits.MINUTE,
|
||||||
|
unit: CoreRemindersUnits.MINUTE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (in seconds).
|
||||||
|
*/
|
||||||
|
async getDefaultNotificationTime(siteId?: string): Promise<number> {
|
||||||
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
|
const key = CoreRemindersService.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId;
|
||||||
|
|
||||||
|
return CoreConfig.get(key, CoreConstants.CONFIG.calendarreminderdefaultvalue || 3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the default notification time.
|
||||||
|
*
|
||||||
|
* @param time New default time.
|
||||||
|
* @param siteId ID of the site. If not defined, use current site.
|
||||||
|
* @return Promise resolved when stored.
|
||||||
|
*/
|
||||||
|
async setDefaultNotificationTime(time: number, siteId?: string): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
|
const key = CoreRemindersService.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId;
|
||||||
|
|
||||||
|
await CoreConfig.set(key, time);
|
||||||
|
|
||||||
|
CoreEvents.trigger(CoreRemindersService.DEFAULT_NOTIFICATION_TIME_CHANGED, { time }, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CoreReminders = makeSingleton(CoreRemindersService);
|
||||||
|
|
||||||
|
export type CoreReminderData = Omit<CoreReminderDBRecord, 'id'>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional data sent in push notifications, with some calculated data.
|
||||||
|
*/
|
||||||
|
export type CoreRemindersPushNotificationData = {
|
||||||
|
reminderId: number;
|
||||||
|
instanceId: number;
|
||||||
|
siteId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CoreReminderNotificationOptions = {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value and unit for reminders.
|
||||||
|
*/
|
||||||
|
export type CoreReminderValueAndUnit = {
|
||||||
|
value: number;
|
||||||
|
unit: CoreRemindersUnits;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CoreReminderSelector = {
|
||||||
|
instanceId: number;
|
||||||
|
component: string;
|
||||||
|
type?: string;
|
||||||
|
};
|
|
@ -0,0 +1,100 @@
|
||||||
|
@app @javascript @core_reminders
|
||||||
|
Feature: Set a new reminder on activity
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given the following "users" exist:
|
||||||
|
| username | firstname | lastname | email |
|
||||||
|
| student1 | Student | student | student1@example.com |
|
||||||
|
And the following "courses" exist:
|
||||||
|
| fullname | shortname | category |
|
||||||
|
| Course 1 | C1 | 0 |
|
||||||
|
And the following "course enrolments" exist:
|
||||||
|
| user | course | role |
|
||||||
|
| student1 | C1 | student |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | course | idnumber | name | allowsubmissionsfromdate | duedate |
|
||||||
|
| assign | C1 | assign01 | Assignment 01 | ## yesterday ## | ## now +70 minutes ## |
|
||||||
|
| assign | C1 | assign02 | Assignment 02 | ## yesterday ## | ## 1 January 2050 ## |
|
||||||
|
|
||||||
|
Scenario: Add, delete and update reminder on activity
|
||||||
|
Given I entered the assign activity "Assignment 01" on course "Course 1" as "student1" in the app
|
||||||
|
|
||||||
|
Then I should not find "Set a reminder for \"Assignment 01\" (Opened)" in the app
|
||||||
|
And I should find "Set a reminder for \"Assignment 01\" (Due)" in the app
|
||||||
|
And "Set a reminder for \"Assignment 01\" (Due)" should not be selected in the app
|
||||||
|
|
||||||
|
# Default set
|
||||||
|
When I press "Set a reminder for \"Assignment 01\" (Due)" in the app
|
||||||
|
Then I should find "Reminder set for " in the app
|
||||||
|
And "Set a reminder for \"Assignment 01\" (Due)" should be selected in the app
|
||||||
|
|
||||||
|
# Set from list
|
||||||
|
When I press "Set a reminder for \"Assignment 01\" (Due)" in the app
|
||||||
|
Then I should find "Set a reminder" in the app
|
||||||
|
And "At the time of the event" should be selected in the app
|
||||||
|
And "1 hour before" should not be selected in the app
|
||||||
|
When I press "1 hour before" in the app
|
||||||
|
Then I should find "Reminder set for " in the app
|
||||||
|
And "Set a reminder for \"Assignment 01\" (Due)" should be selected in the app
|
||||||
|
|
||||||
|
# Custom set
|
||||||
|
When I press "Set a reminder for \"Assignment 01\" (Due)" in the app
|
||||||
|
Then I should find "Set a reminder" in the app
|
||||||
|
And "At the time of the event" should not be selected in the app
|
||||||
|
And "1 hour before" should be selected in the app
|
||||||
|
When I press "Custom..." in the app
|
||||||
|
Then I should find "Custom reminder" in the app
|
||||||
|
When I set the following fields to these values in the app:
|
||||||
|
| Value | 4 |
|
||||||
|
| Units | minutes |
|
||||||
|
And I press "Set reminder" in the app
|
||||||
|
Then I should find "Reminder set for " in the app
|
||||||
|
And "Set a reminder for \"Assignment 01\" (Due)" should be selected in the app
|
||||||
|
|
||||||
|
# Remove
|
||||||
|
When I press "Set a reminder for \"Assignment 01\" (Due)" in the app
|
||||||
|
Then "4 minutes before" should be selected in the app
|
||||||
|
When I press "Delete reminder" in the app
|
||||||
|
Then I should find "Reminder deleted" in the app
|
||||||
|
And "Set a reminder for \"Assignment 01\" (Due)" should not be selected in the app
|
||||||
|
|
||||||
|
# Set and check reminder
|
||||||
|
When I press "Set a reminder for \"Assignment 01\" (Due)" in the app
|
||||||
|
Then I should find "Reminder set for " in the app
|
||||||
|
When I press "Set a reminder for \"Assignment 01\" (Due)" in the app
|
||||||
|
And I press "Custom..." in the app
|
||||||
|
Then I should find "Custom reminder" in the app
|
||||||
|
When I set the following fields to these values in the app:
|
||||||
|
| Value | 69 |
|
||||||
|
| Units | minutes |
|
||||||
|
And I press "Set reminder" in the app
|
||||||
|
Then I should find "Reminder set for " in the app
|
||||||
|
When I wait "50" seconds
|
||||||
|
Then a notification with title "Due: Assignment 01" is present in the app
|
||||||
|
And I close a notification with title "Due: Assignment 01" in the app
|
||||||
|
|
||||||
|
# Set and check reminder is cancelled
|
||||||
|
When I press "Set a reminder for \"Assignment 01\" (Due)" in the app
|
||||||
|
And I press "Custom..." in the app
|
||||||
|
Then I should find "Custom reminder" in the app
|
||||||
|
When I set the following fields to these values in the app:
|
||||||
|
| Value | 68 |
|
||||||
|
| Units | minutes |
|
||||||
|
And I press "Set reminder" in the app
|
||||||
|
Then I should find "Reminder set for " in the app
|
||||||
|
When I press "Set a reminder for \"Assignment 01\" (Due)" in the app
|
||||||
|
Then I should find "Reminder set for " in the app
|
||||||
|
When I press "Delete reminder" in the app
|
||||||
|
Then I should find "Reminder deleted" in the app
|
||||||
|
When I wait "50" seconds
|
||||||
|
Then a notification with title "Due: Assignment 01" is not present in the app
|
||||||
|
|
||||||
|
Scenario: Check toast is correct
|
||||||
|
Given I entered the assign activity "Assignment 02" on course "Course 1" as "student1" in the app
|
||||||
|
|
||||||
|
When I press "Set a reminder for \"Assignment 02\" (Due)" in the app
|
||||||
|
Then I should find "Reminder set for " in the app
|
||||||
|
|
||||||
|
When I press "Set a reminder for \"Assignment 02\" (Due)" in the app
|
||||||
|
And I press "1 day before" in the app
|
||||||
|
Then I should find "Reminder set for Friday, 31 December 2049, 12:00 AM" in the app
|
|
@ -0,0 +1,56 @@
|
||||||
|
@app @javascript @core_reminders
|
||||||
|
Feature: Set a new reminder on course
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given the following "users" exist:
|
||||||
|
| username | firstname | lastname | email |
|
||||||
|
| student1 | Student | student | student1@example.com |
|
||||||
|
And the following "courses" exist:
|
||||||
|
| fullname | shortname | category | startdate | enddate |
|
||||||
|
| Course 1 | C1 | 0 | ## yesterday ## | ## now +24 hours ## |
|
||||||
|
And the following "course enrolments" exist:
|
||||||
|
| user | course | role |
|
||||||
|
| student1 | C1 | student |
|
||||||
|
|
||||||
|
Scenario: Add, delete and update reminder on course
|
||||||
|
Given I entered the course "Course 1" as "student1" in the app
|
||||||
|
And I press "Course summary" in the app
|
||||||
|
|
||||||
|
Then I should not find "Set a reminder for \"Course 1\" (Course start date)" in the app
|
||||||
|
And I should find "Set a reminder for \"Course 1\" (Course end date)" in the app
|
||||||
|
And "Set a reminder for \"Course 1\" (Course end date)" should not be selected in the app
|
||||||
|
|
||||||
|
# Default set
|
||||||
|
When I press "Set a reminder for \"Course 1\" (Course end date)" in the app
|
||||||
|
Then I should find "Reminder set for " in the app
|
||||||
|
And "Set a reminder for \"Course 1\" (Course end date)" should be selected in the app
|
||||||
|
|
||||||
|
# Set from list
|
||||||
|
When I press "Set a reminder for \"Course 1\" (Course end date)" in the app
|
||||||
|
Then I should find "Set a reminder" in the app
|
||||||
|
And "At the time of the event" should be selected in the app
|
||||||
|
And "12 hours before" should not be selected in the app
|
||||||
|
When I press "12 hours before" in the app
|
||||||
|
Then I should find "Reminder set for " in the app
|
||||||
|
And "Set a reminder for \"Course 1\" (Course end date)" should be selected in the app
|
||||||
|
|
||||||
|
# Custom set
|
||||||
|
When I press "Set a reminder for \"Course 1\" (Course end date)" in the app
|
||||||
|
Then I should find "Set a reminder" in the app
|
||||||
|
And "At the time of the event" should not be selected in the app
|
||||||
|
And "12 hours before" should be selected in the app
|
||||||
|
When I press "Custom..." in the app
|
||||||
|
Then I should find "Custom reminder" in the app
|
||||||
|
When I set the following fields to these values in the app:
|
||||||
|
| Value | 2 |
|
||||||
|
| Units | hours |
|
||||||
|
And I press "Set reminder" in the app
|
||||||
|
Then I should find "Reminder set for " in the app
|
||||||
|
And "Set a reminder for \"Course 1\" (Course end date)" should be selected in the app
|
||||||
|
|
||||||
|
# Remove
|
||||||
|
When I press "Set a reminder for \"Course 1\" (Course end date)" in the app
|
||||||
|
Then "2 hours before" should be selected in the app
|
||||||
|
When I press "Delete reminder" in the app
|
||||||
|
Then I should find "Reminder deleted" in the app
|
||||||
|
And "Set a reminder for \"Course 1\" (Course end date)" should not be selected in the app
|
|
@ -94,7 +94,7 @@ export class CoreSettingsDeviceInfoPage implements OnDestroy {
|
||||||
lastCommit: CoreConstants.BUILD.lastCommitHash || '',
|
lastCommit: CoreConstants.BUILD.lastCommitHash || '',
|
||||||
networkStatus: CoreNetwork.isOnline() ? 'online' : 'offline',
|
networkStatus: CoreNetwork.isOnline() ? 'online' : 'offline',
|
||||||
wifiConnection: CoreNetwork.isWifi() ? 'yes' : 'no',
|
wifiConnection: CoreNetwork.isWifi() ? 'yes' : 'no',
|
||||||
localNotifAvailable: CoreLocalNotifications.isAvailable() ? 'yes' : 'no',
|
localNotifAvailable: CoreLocalNotifications.isPluginAvailable() ? 'yes' : 'no',
|
||||||
pushId: CorePushNotifications.getPushId(),
|
pushId: CorePushNotifications.getPushId(),
|
||||||
deviceType: '',
|
deviceType: '',
|
||||||
};
|
};
|
||||||
|
|
|
@ -138,6 +138,9 @@ export class CoreAppProvider {
|
||||||
if (schema.tables) {
|
if (schema.tables) {
|
||||||
await this.getDB().createTablesFromSchema(schema.tables);
|
await this.getDB().createTablesFromSchema(schema.tables);
|
||||||
}
|
}
|
||||||
|
if (schema.install && oldVersion === 0) {
|
||||||
|
await schema.install(this.getDB());
|
||||||
|
}
|
||||||
if (schema.migrate && oldVersion > 0) {
|
if (schema.migrate && oldVersion > 0) {
|
||||||
await schema.migrate(this.getDB(), oldVersion);
|
await schema.migrate(this.getDB(), oldVersion);
|
||||||
}
|
}
|
||||||
|
@ -662,7 +665,7 @@ export class CoreAppProvider {
|
||||||
const entry = await this.schemaVersionsTable.getOneByPrimaryKey({ name: schema.name });
|
const entry = await this.schemaVersionsTable.getOneByPrimaryKey({ name: schema.name });
|
||||||
|
|
||||||
return entry.version;
|
return entry.version;
|
||||||
} catch (error) {
|
} catch {
|
||||||
// No installed version yet.
|
// No installed version yet.
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -727,11 +730,21 @@ export type CoreAppSchema = {
|
||||||
/**
|
/**
|
||||||
* Migrates the schema to the latest version.
|
* Migrates the schema to the latest version.
|
||||||
*
|
*
|
||||||
* Called when installing and upgrading the schema, after creating the defined tables.
|
* Called when upgrading the schema, after creating the defined tables.
|
||||||
*
|
*
|
||||||
* @param db The affected DB.
|
* @param db The affected DB.
|
||||||
* @param oldVersion Old version of the schema or 0 if not installed.
|
* @param oldVersion Old version of the schema or 0 if not installed.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
migrate?(db: SQLiteDB, oldVersion: number): Promise<void>;
|
migrate?(db: SQLiteDB, oldVersion: number): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make changes to install the schema.
|
||||||
|
*
|
||||||
|
* Called when installing the schema, after creating the defined tables.
|
||||||
|
*
|
||||||
|
* @param db Site database.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
install?(db: SQLiteDB): Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
|
@ -123,7 +123,7 @@ export class CoreLocalNotificationsProvider {
|
||||||
async initializeDatabase(): Promise<void> {
|
async initializeDatabase(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await CoreApp.createTablesFromSchema(APP_SCHEMA);
|
await CoreApp.createTablesFromSchema(APP_SCHEMA);
|
||||||
} catch (e) {
|
} catch {
|
||||||
// Ignore errors.
|
// Ignore errors.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,14 +319,26 @@ export class CoreLocalNotificationsProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether local notifications plugin is installed.
|
* Returns whether local notifications are available.
|
||||||
*
|
*
|
||||||
* @return Whether local notifications plugin is installed.
|
* @return Whether local notifications are available.
|
||||||
|
* @deprecated since 4.1. It will always return true.
|
||||||
*/
|
*/
|
||||||
isAvailable(): boolean {
|
isAvailable(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether local notifications plugin is available.
|
||||||
|
*
|
||||||
|
* @return Whether local notifications plugin is available.
|
||||||
|
*/
|
||||||
|
isPluginAvailable(): boolean {
|
||||||
const win = <any> window; // eslint-disable-line @typescript-eslint/no-explicit-any
|
const win = <any> window; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
|
||||||
return !!win.cordova?.plugins?.notification?.local;
|
const enabled = !!win.cordova?.plugins?.notification?.local;
|
||||||
|
|
||||||
|
return enabled && CorePlatform.is('cordova');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -84,6 +84,7 @@ export class CoreSitesProvider {
|
||||||
protected sessionRestored = false;
|
protected sessionRestored = false;
|
||||||
protected currentSite?: CoreSite;
|
protected currentSite?: CoreSite;
|
||||||
protected sites: { [s: string]: CoreSite } = {};
|
protected sites: { [s: string]: CoreSite } = {};
|
||||||
|
protected sitesMigrated: { [s: string]: boolean } = {};
|
||||||
protected siteSchemasMigration: { [siteId: string]: Promise<void> } = {};
|
protected siteSchemasMigration: { [siteId: string]: Promise<void> } = {};
|
||||||
protected siteSchemas: { [name: string]: CoreRegisteredSiteSchema } = {};
|
protected siteSchemas: { [name: string]: CoreRegisteredSiteSchema } = {};
|
||||||
protected pluginsSiteSchemas: { [name: string]: CoreRegisteredSiteSchema } = {};
|
protected pluginsSiteSchemas: { [name: string]: CoreRegisteredSiteSchema } = {};
|
||||||
|
@ -128,7 +129,7 @@ export class CoreSitesProvider {
|
||||||
async initializeDatabase(): Promise<void> {
|
async initializeDatabase(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await CoreApp.createTablesFromSchema(APP_SCHEMA);
|
await CoreApp.createTablesFromSchema(APP_SCHEMA);
|
||||||
} catch (e) {
|
} catch {
|
||||||
// Ignore errors.
|
// Ignore errors.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,7 +219,9 @@ export class CoreSitesProvider {
|
||||||
|
|
||||||
if (!CoreUrlUtils.isHttpURL(siteUrl)) {
|
if (!CoreUrlUtils.isHttpURL(siteUrl)) {
|
||||||
throw new CoreError(Translate.instant('core.login.invalidsite'));
|
throw new CoreError(Translate.instant('core.login.invalidsite'));
|
||||||
} else if (!CoreNetwork.isOnline()) {
|
}
|
||||||
|
|
||||||
|
if (!CoreNetwork.isOnline()) {
|
||||||
throw new CoreNetworkError();
|
throw new CoreNetworkError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,14 +306,18 @@ export class CoreSitesProvider {
|
||||||
errorDetails: Translate.instant('core.login.webservicesnotenabled'),
|
errorDetails: Translate.instant('core.login.webservicesnotenabled'),
|
||||||
critical: true,
|
critical: true,
|
||||||
});
|
});
|
||||||
} else if (!config.enablemobilewebservice) {
|
}
|
||||||
|
|
||||||
|
if (!config.enablemobilewebservice) {
|
||||||
throw this.createCannotConnectLoginError(config.httpswwwroot || config.wwwroot, {
|
throw this.createCannotConnectLoginError(config.httpswwwroot || config.wwwroot, {
|
||||||
supportConfig: new CoreUserGuestSupportConfig(config),
|
supportConfig: new CoreUserGuestSupportConfig(config),
|
||||||
errorcode: 'mobileservicesnotenabled',
|
errorcode: 'mobileservicesnotenabled',
|
||||||
errorDetails: Translate.instant('core.login.mobileservicesnotenabled'),
|
errorDetails: Translate.instant('core.login.mobileservicesnotenabled'),
|
||||||
critical: true,
|
critical: true,
|
||||||
});
|
});
|
||||||
} else if (config.maintenanceenabled) {
|
}
|
||||||
|
|
||||||
|
if (config.maintenanceenabled) {
|
||||||
let message = Translate.instant('core.sitemaintenance');
|
let message = Translate.instant('core.sitemaintenance');
|
||||||
if (config.maintenancemessage) {
|
if (config.maintenancemessage) {
|
||||||
message += config.maintenancemessage;
|
message += config.maintenancemessage;
|
||||||
|
@ -497,39 +504,41 @@ export class CoreSitesProvider {
|
||||||
? Translate.instant('core.siteunavailablehelp', { site: this.currentSite?.siteUrl })
|
? Translate.instant('core.siteunavailablehelp', { site: this.currentSite?.siteUrl })
|
||||||
: Translate.instant('core.sitenotfoundhelp'),
|
: Translate.instant('core.sitenotfoundhelp'),
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
if (data.token !== undefined) {
|
|
||||||
return { token: data.token, siteUrl, privateToken: data.privatetoken };
|
|
||||||
} else {
|
|
||||||
if (data.error !== undefined) {
|
|
||||||
// We only allow one retry (to avoid loops).
|
|
||||||
if (!retry && data.errorcode == 'requirecorrectaccess') {
|
|
||||||
siteUrl = CoreUrlUtils.addOrRemoveWWW(siteUrl);
|
|
||||||
|
|
||||||
return this.getUserToken(siteUrl, username, password, service, true);
|
if (data.token !== undefined) {
|
||||||
} else if (data.errorcode == 'missingparam') {
|
return { token: data.token, siteUrl, privateToken: data.privatetoken };
|
||||||
// It seems the server didn't receive all required params, it could be due to a redirect.
|
}
|
||||||
const redirect = await CoreUtils.checkRedirect(loginUrl);
|
|
||||||
|
|
||||||
if (redirect) {
|
if (data.error === undefined) {
|
||||||
throw this.createCannotConnectLoginError(siteUrl, {
|
throw new CoreError(Translate.instant('core.login.invalidaccount'));
|
||||||
supportConfig: await CoreUserGuestSupportConfig.forSite(siteUrl),
|
}
|
||||||
errorcode: 'sitehasredirect',
|
|
||||||
errorDetails: Translate.instant('core.login.sitehasredirect'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw this.createCannotConnectLoginError(siteUrl, {
|
// We only allow one retry (to avoid loops).
|
||||||
supportConfig: await CoreUserGuestSupportConfig.forSite(siteUrl),
|
if (!retry && data.errorcode == 'requirecorrectaccess') {
|
||||||
errorcode: data.errorcode,
|
siteUrl = CoreUrlUtils.addOrRemoveWWW(siteUrl);
|
||||||
errorDetails: data.error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new CoreError(Translate.instant('core.login.invalidaccount'));
|
return this.getUserToken(siteUrl, username, password, service, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.errorcode == 'missingparam') {
|
||||||
|
// It seems the server didn't receive all required params, it could be due to a redirect.
|
||||||
|
const redirect = await CoreUtils.checkRedirect(loginUrl);
|
||||||
|
|
||||||
|
if (redirect) {
|
||||||
|
throw this.createCannotConnectLoginError(siteUrl, {
|
||||||
|
supportConfig: await CoreUserGuestSupportConfig.forSite(siteUrl),
|
||||||
|
errorcode: 'sitehasredirect',
|
||||||
|
errorDetails: Translate.instant('core.login.sitehasredirect'),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw this.createCannotConnectLoginError(siteUrl, {
|
||||||
|
supportConfig: await CoreUserGuestSupportConfig.forSite(siteUrl),
|
||||||
|
errorcode: data.errorcode,
|
||||||
|
errorDetails: data.error,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -549,19 +558,19 @@ export class CoreSitesProvider {
|
||||||
login: boolean = true,
|
login: boolean = true,
|
||||||
oauthId?: number,
|
oauthId?: number,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (typeof login != 'boolean') {
|
if (typeof login !== 'boolean') {
|
||||||
login = true;
|
login = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a "candidate" site to fetch the site info.
|
// Create a "candidate" site to fetch the site info.
|
||||||
let candidateSite = CoreSitesFactory.makeSite(undefined, siteUrl, token, undefined, privateToken, undefined, undefined);
|
let candidateSite = CoreSitesFactory.makeSite(undefined, siteUrl, token, undefined, privateToken);
|
||||||
let isNewSite = true;
|
let isNewSite = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const info = await candidateSite.fetchSiteInfo();
|
const info = await candidateSite.fetchSiteInfo();
|
||||||
|
|
||||||
const result = this.isValidMoodleVersion(info);
|
const result = this.isValidMoodleVersion(info);
|
||||||
if (result != CoreSitesProvider.VALID_VERSION) {
|
if (result !== CoreSitesProvider.VALID_VERSION) {
|
||||||
return this.treatInvalidAppVersion(result);
|
return this.treatInvalidAppVersion(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1080,19 +1089,28 @@ export class CoreSitesProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new CoreError('No current site found.');
|
throw new CoreError('No current site found.');
|
||||||
} else if (this.currentSite && this.currentSite.getId() == siteId) {
|
}
|
||||||
return this.currentSite;
|
|
||||||
} else if (this.sites[siteId] !== undefined) {
|
|
||||||
return this.sites[siteId];
|
|
||||||
} else {
|
|
||||||
// Retrieve and create the site.
|
|
||||||
try {
|
|
||||||
const data = await this.sitesTable.getOneByPrimaryKey({ id: siteId });
|
|
||||||
|
|
||||||
return this.addSiteFromSiteListEntry(data);
|
if (this.currentSite && this.currentSite.getId() === siteId) {
|
||||||
} catch {
|
return this.currentSite;
|
||||||
throw new CoreError('SiteId not found');
|
}
|
||||||
}
|
|
||||||
|
if (this.sites[siteId] !== undefined) {
|
||||||
|
return this.sites[siteId];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve and create the site.
|
||||||
|
let record: SiteDBEntry;
|
||||||
|
try {
|
||||||
|
record = await this.sitesTable.getOneByPrimaryKey({ id: siteId });
|
||||||
|
} catch {
|
||||||
|
throw new CoreError('SiteId not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.addSiteFromSiteListEntry(record);
|
||||||
|
} catch {
|
||||||
|
throw new CoreError('Site database installation or update failed.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1123,10 +1141,6 @@ export class CoreSitesProvider {
|
||||||
async getSiteByUrl(siteUrl: string): Promise<CoreSite> {
|
async getSiteByUrl(siteUrl: string): Promise<CoreSite> {
|
||||||
const data = await this.sitesTable.getOne({ siteUrl });
|
const data = await this.sitesTable.getOne({ siteUrl });
|
||||||
|
|
||||||
if (this.sites[data.id] !== undefined) {
|
|
||||||
return this.sites[data.id];
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.addSiteFromSiteListEntry(data);
|
return this.addSiteFromSiteListEntry(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1148,16 +1162,20 @@ export class CoreSitesProvider {
|
||||||
* @param entry Site list entry.
|
* @param entry Site list entry.
|
||||||
* @return Promised resolved with the created site.
|
* @return Promised resolved with the created site.
|
||||||
*/
|
*/
|
||||||
addSiteFromSiteListEntry(entry: SiteDBEntry): Promise<CoreSite> {
|
async addSiteFromSiteListEntry(entry: SiteDBEntry): Promise<CoreSite> {
|
||||||
|
if (this.sites[entry.id] !== undefined) {
|
||||||
|
return this.sites[entry.id];
|
||||||
|
}
|
||||||
|
|
||||||
// Parse info and config.
|
// Parse info and config.
|
||||||
const site = this.makeSiteFromSiteListEntry(entry);
|
const site = this.makeSiteFromSiteListEntry(entry);
|
||||||
|
|
||||||
return this.migrateSiteSchemas(site).then(() => {
|
await this.migrateSiteSchemas(site);
|
||||||
// Set site after migrating schemas, or a call to getSite could get the site while tables are being created.
|
|
||||||
this.sites[entry.id] = site;
|
|
||||||
|
|
||||||
return site;
|
// Set site after migrating schemas, or a call to getSite could get the site while tables are being created.
|
||||||
});
|
this.sites[entry.id] = site;
|
||||||
|
|
||||||
|
return site;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1167,8 +1185,8 @@ export class CoreSitesProvider {
|
||||||
* @return Site.
|
* @return Site.
|
||||||
*/
|
*/
|
||||||
makeSiteFromSiteListEntry(entry: SiteDBEntry): CoreSite {
|
makeSiteFromSiteListEntry(entry: SiteDBEntry): CoreSite {
|
||||||
const info = entry.info ? <CoreSiteInfo> CoreTextUtils.parseJSON(entry.info) : undefined;
|
const info = entry.info ? CoreTextUtils.parseJSON<CoreSiteInfo>(entry.info) : undefined;
|
||||||
const config = entry.config ? <CoreSiteConfig> CoreTextUtils.parseJSON(entry.config) : undefined;
|
const config = entry.config ? CoreTextUtils.parseJSON<CoreSiteConfig>(entry.config) : undefined;
|
||||||
|
|
||||||
const site = CoreSitesFactory.makeSite(
|
const site = CoreSitesFactory.makeSite(
|
||||||
entry.id,
|
entry.id,
|
||||||
|
@ -1321,13 +1339,13 @@ export class CoreSitesProvider {
|
||||||
async getSitesInstances(): Promise<CoreSite[]> {
|
async getSitesInstances(): Promise<CoreSite[]> {
|
||||||
const siteIds = await this.getSitesIds();
|
const siteIds = await this.getSitesIds();
|
||||||
|
|
||||||
return Promise.all(siteIds.map(async (siteId) => await this.getSite(siteId)));
|
return Promise.all(siteIds.map((siteId) => this.getSite(siteId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login the user in a site.
|
* Login the user in a site.
|
||||||
*
|
*
|
||||||
* @param siteid ID of the site the user is accessing.
|
* @param siteId ID of the site the user is accessing.
|
||||||
* @return Promise resolved when current site is stored.
|
* @return Promise resolved when current site is stored.
|
||||||
*/
|
*/
|
||||||
async login(siteId: string): Promise<void> {
|
async login(siteId: string): Promise<void> {
|
||||||
|
@ -1479,7 +1497,7 @@ export class CoreSitesProvider {
|
||||||
/**
|
/**
|
||||||
* Updates a site's info.
|
* Updates a site's info.
|
||||||
*
|
*
|
||||||
* @param siteid Site's ID.
|
* @param siteId Site's ID.
|
||||||
* @return A promise resolved when the site is updated.
|
* @return A promise resolved when the site is updated.
|
||||||
*/
|
*/
|
||||||
async updateSiteInfo(siteId?: string): Promise<void> {
|
async updateSiteInfo(siteId?: string): Promise<void> {
|
||||||
|
@ -1500,7 +1518,7 @@ export class CoreSitesProvider {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
config = await this.getSiteConfig(site);
|
config = await this.getSiteConfig(site);
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Error getting config, keep the current one.
|
// Error getting config, keep the current one.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1561,14 +1579,14 @@ export class CoreSitesProvider {
|
||||||
if (CoreUrlUtils.isAbsoluteURL(url)) {
|
if (CoreUrlUtils.isAbsoluteURL(url)) {
|
||||||
// It has some protocol. Return empty array.
|
// It has some protocol. Return empty array.
|
||||||
return [];
|
return [];
|
||||||
} else {
|
|
||||||
// No protocol, probably a relative URL. Return current site.
|
|
||||||
if (this.currentSite) {
|
|
||||||
return [this.currentSite.getId()];
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No protocol, probably a relative URL. Return current site.
|
||||||
|
if (this.currentSite) {
|
||||||
|
return [this.currentSite.getId()];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -1576,9 +1594,7 @@ export class CoreSitesProvider {
|
||||||
const ids: string[] = [];
|
const ids: string[] = [];
|
||||||
|
|
||||||
await Promise.all(siteEntries.map(async (site) => {
|
await Promise.all(siteEntries.map(async (site) => {
|
||||||
if (!this.sites[site.id]) {
|
await this.addSiteFromSiteListEntry(site);
|
||||||
await this.addSiteFromSiteListEntry(site);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.sites[site.id].containsUrl(url)) {
|
if (this.sites[site.id].containsUrl(url)) {
|
||||||
if (!username || this.sites[site.id].getInfo()?.username === username) {
|
if (!username || this.sites[site.id].getInfo()?.username === username) {
|
||||||
|
@ -1694,24 +1710,30 @@ export class CoreSitesProvider {
|
||||||
* @param site Site.
|
* @param site Site.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
migrateSiteSchemas(site: CoreSite): Promise<void> {
|
async migrateSiteSchemas(site: CoreSite): Promise<void> {
|
||||||
if (!site.id) {
|
if (!site.id) {
|
||||||
return Promise.resolve();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const siteId = site.id;
|
const siteId = site.id;
|
||||||
|
|
||||||
if (this.siteSchemasMigration[site.id] !== undefined) {
|
if (this.siteSchemasMigration[siteId] !== undefined) {
|
||||||
return this.siteSchemasMigration[site.id];
|
return this.siteSchemasMigration[siteId];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(`Migrating all schemas of ${site.id}`);
|
if (this.sitesMigrated[siteId] !== undefined) {
|
||||||
|
throw new CoreError('Site already migrated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sitesMigrated[siteId] = true;
|
||||||
|
|
||||||
|
this.logger.debug(`Migrating all schemas of ${siteId}`);
|
||||||
|
|
||||||
// First create tables not registerd with name/version.
|
// First create tables not registerd with name/version.
|
||||||
const promise = site.getDb().createTableFromSchema(SCHEMA_VERSIONS_TABLE_SCHEMA)
|
const promise = site.getDb().createTableFromSchema(SCHEMA_VERSIONS_TABLE_SCHEMA)
|
||||||
.then(() => this.applySiteSchemas(site, this.siteSchemas));
|
.then(() => this.applySiteSchemas(site, this.siteSchemas));
|
||||||
|
|
||||||
this.siteSchemasMigration[site.id] = promise;
|
this.siteSchemasMigration[siteId] = promise;
|
||||||
|
|
||||||
return promise.finally(() => {
|
return promise.finally(() => {
|
||||||
delete this.siteSchemasMigration[siteId];
|
delete this.siteSchemasMigration[siteId];
|
||||||
|
@ -1769,6 +1791,9 @@ export class CoreSitesProvider {
|
||||||
if (schema.tables) {
|
if (schema.tables) {
|
||||||
await db.createTablesFromSchema(schema.tables);
|
await db.createTablesFromSchema(schema.tables);
|
||||||
}
|
}
|
||||||
|
if (schema.install && oldVersion == 0) {
|
||||||
|
await schema.install(db, site.id);
|
||||||
|
}
|
||||||
if (schema.migrate && oldVersion > 0) {
|
if (schema.migrate && oldVersion > 0) {
|
||||||
await schema.migrate(db, oldVersion, site.id);
|
await schema.migrate(db, oldVersion, site.id);
|
||||||
}
|
}
|
||||||
|
@ -2032,7 +2057,7 @@ export type CoreSiteSchema = {
|
||||||
/**
|
/**
|
||||||
* Migrates the schema in a site to the latest version.
|
* Migrates the schema in a site to the latest version.
|
||||||
*
|
*
|
||||||
* Called when installing and upgrading the schema, after creating the defined tables.
|
* Called when upgrading the schema, after creating the defined tables.
|
||||||
*
|
*
|
||||||
* @param db Site database.
|
* @param db Site database.
|
||||||
* @param oldVersion Old version of the schema or 0 if not installed.
|
* @param oldVersion Old version of the schema or 0 if not installed.
|
||||||
|
@ -2040,6 +2065,17 @@ export type CoreSiteSchema = {
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
migrate?(db: SQLiteDB, oldVersion: number, siteId: string): Promise<void> | void;
|
migrate?(db: SQLiteDB, oldVersion: number, siteId: string): Promise<void> | void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make changes to install the schema in a site.
|
||||||
|
*
|
||||||
|
* Called when installing the schema, after creating the defined tables.
|
||||||
|
*
|
||||||
|
* @param db Site database.
|
||||||
|
* @param siteId Site Id to migrate.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
install?(db: SQLiteDB, siteId: string): Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1138,7 +1138,7 @@ export class CoreDomUtilsProvider {
|
||||||
/**
|
/**
|
||||||
* Show an alert modal with a button to close it.
|
* Show an alert modal with a button to close it.
|
||||||
*
|
*
|
||||||
* @param title Title to show.
|
* @param header Title to show.
|
||||||
* @param message Message to show.
|
* @param message Message to show.
|
||||||
* @param buttonText Text of the button.
|
* @param buttonText Text of the button.
|
||||||
* @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed.
|
* @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed.
|
||||||
|
@ -1230,7 +1230,7 @@ export class CoreDomUtilsProvider {
|
||||||
/**
|
/**
|
||||||
* Show an alert modal with a button to close it, translating the values supplied.
|
* Show an alert modal with a button to close it, translating the values supplied.
|
||||||
*
|
*
|
||||||
* @param title Title to show.
|
* @param header Title to show.
|
||||||
* @param message Message to show.
|
* @param message Message to show.
|
||||||
* @param buttonText Text of the button.
|
* @param buttonText Text of the button.
|
||||||
* @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed.
|
* @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed.
|
||||||
|
@ -1669,7 +1669,6 @@ export class CoreDomUtilsProvider {
|
||||||
* @param needsTranslate Whether the 'text' needs to be translated.
|
* @param needsTranslate Whether the 'text' needs to be translated.
|
||||||
* @param duration Duration in ms of the dimissable toast.
|
* @param duration Duration in ms of the dimissable toast.
|
||||||
* @param cssClass Class to add to the toast.
|
* @param cssClass Class to add to the toast.
|
||||||
* @param dismissOnPageChange Dismiss the Toast on page change.
|
|
||||||
* @return Toast instance.
|
* @return Toast instance.
|
||||||
*/
|
*/
|
||||||
async showToast(
|
async showToast(
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { CoreCustomURLSchemes, CoreCustomURLSchemesProvider } from '@services/ur
|
||||||
import { CoreLoginHelperProvider } from '@features/login/services/login-helper';
|
import { CoreLoginHelperProvider } from '@features/login/services/login-helper';
|
||||||
import { CoreConfig } from '@services/config';
|
import { CoreConfig } from '@services/config';
|
||||||
import { EnvironmentConfig } from '@/types/config';
|
import { EnvironmentConfig } from '@/types/config';
|
||||||
import { makeSingleton, NgZone } from '@singletons';
|
import { LocalNotifications, makeSingleton, NgZone } from '@singletons';
|
||||||
import { CoreNetwork, CoreNetworkService } from '@services/network';
|
import { CoreNetwork, CoreNetworkService } from '@services/network';
|
||||||
import { CorePushNotifications, CorePushNotificationsProvider } from '@features/pushnotifications/services/pushnotifications';
|
import { CorePushNotifications, CorePushNotificationsProvider } from '@features/pushnotifications/services/pushnotifications';
|
||||||
import { CoreCronDelegate, CoreCronDelegateService } from '@services/cron';
|
import { CoreCronDelegate, CoreCronDelegateService } from '@services/cron';
|
||||||
|
@ -491,6 +491,49 @@ export class TestingBehatRuntimeService {
|
||||||
console.log('BEHAT: ' + nowFormatted, ...args); // eslint-disable-line no-console
|
console.log('BEHAT: ' + nowFormatted, ...args); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check a notification is present.
|
||||||
|
*
|
||||||
|
* @param title Title of the notification
|
||||||
|
* @return YES or NO: depending on the result.
|
||||||
|
*/
|
||||||
|
async notificationIsPresentWithText(title: string): Promise<string> {
|
||||||
|
const notifications = await LocalNotifications.getAllTriggered();
|
||||||
|
|
||||||
|
const notification = notifications.find((notification) => notification.title?.includes(title));
|
||||||
|
|
||||||
|
if (!notification) {
|
||||||
|
return 'NO';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!notification.id) {
|
||||||
|
// Cannot check but has been triggered.
|
||||||
|
return 'YES';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await LocalNotifications.isPresent(notification.id)) ? 'YES' : 'NO';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close notification.
|
||||||
|
*
|
||||||
|
* @param title Title of the notification
|
||||||
|
* @return OK or ERROR
|
||||||
|
*/
|
||||||
|
async closeNotification(title: string): Promise<string> {
|
||||||
|
const notifications = await LocalNotifications.getAllTriggered();
|
||||||
|
|
||||||
|
const notification = notifications.find((notification) => notification.title?.includes(title));
|
||||||
|
|
||||||
|
if (!notification || !notification.id) {
|
||||||
|
return `ERROR: Notification with title ${title} cannot be closed`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await LocalNotifications.clear(notification.id);
|
||||||
|
|
||||||
|
return 'OK';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TestingBehatRuntime = makeSingleton(TestingBehatRuntimeService);
|
export const TestingBehatRuntime = makeSingleton(TestingBehatRuntimeService);
|
||||||
|
|
Loading…
Reference in New Issue