MOBILE-3909 calendar: Change reminders to time before

main
Dani Palou 2021-11-10 13:30:31 +01:00
parent 4fa1d72a2b
commit 47c913f434
13 changed files with 581 additions and 112 deletions

View File

@ -140,6 +140,7 @@
"addon.calendar.sunday": "calendar",
"addon.calendar.thu": "calendar",
"addon.calendar.thursday": "calendar",
"addon.calendar.timebefore": "local_moodlemobileapp",
"addon.calendar.today": "calendar",
"addon.calendar.tomorrow": "calendar",
"addon.calendar.tue": "calendar",
@ -153,6 +154,7 @@
"addon.calendar.typeopen": "calendar",
"addon.calendar.typesite": "calendar",
"addon.calendar.typeuser": "calendar",
"addon.calendar.units": "qtype_numerical",
"addon.calendar.upcomingevents": "calendar",
"addon.calendar.userevents": "calendar",
"addon.calendar.wed": "calendar",
@ -1562,6 +1564,7 @@
"core.courses.therearecourses": "moodle",
"core.courses.totalcoursesearchresults": "local_moodlemobileapp",
"core.currentdevice": "local_moodlemobileapp",
"core.custom": "form",
"core.datastoredoffline": "local_moodlemobileapp",
"core.date": "moodle",
"core.day": "moodle",
@ -1943,6 +1946,8 @@
"core.maxsizeandattachments": "moodle",
"core.min": "moodle",
"core.mins": "moodle",
"core.minute": "moodle",
"core.minutes": "moodle",
"core.misc": "admin",
"core.mod_assign": "assign/pluginname",
"core.mod_assignment": "assignment/pluginname",
@ -2283,6 +2288,8 @@
"core.viewprofile": "moodle",
"core.warningofflinedatadeleted": "local_moodlemobileapp",
"core.warnopeninbrowser": "local_moodlemobileapp",
"core.week": "moodle",
"core.weeks": "moodle",
"core.whatisyourage": "moodle",
"core.wheredoyoulive": "moodle",
"core.whoissiteadmin": "local_moodlemobileapp",

View File

@ -19,12 +19,14 @@ import { CoreSharedModule } from '@/core/shared.module';
import { AddonCalendarCalendarComponent } from './calendar/calendar';
import { AddonCalendarUpcomingEventsComponent } from './upcoming-events/upcoming-events';
import { AddonCalendarFilterPopoverComponent } from './filter/filter';
import { AddonCalendarReminderTimeModalComponent } from './reminder-time-modal/reminder-time-modal';
@NgModule({
declarations: [
AddonCalendarCalendarComponent,
AddonCalendarUpcomingEventsComponent,
AddonCalendarFilterPopoverComponent,
AddonCalendarReminderTimeModalComponent,
],
imports: [
CoreSharedModule,
@ -35,6 +37,7 @@ import { AddonCalendarFilterPopoverComponent } from './filter/filter';
AddonCalendarCalendarComponent,
AddonCalendarUpcomingEventsComponent,
AddonCalendarFilterPopoverComponent,
AddonCalendarReminderTimeModalComponent,
],
})
export class AddonCalendarComponentsModule {}

View File

@ -0,0 +1,59 @@
<ion-header>
<ion-toolbar>
<h2>{{ 'addon.calendar.reminders' | translate }}</h2>
<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>
<ion-radio-group [(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 lines="none" 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">
<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">
</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 class="ion-margin" expand="block" (click)="saveReminder()" [disabled]="radioValue == 'custom' && !customValue">
{{ 'core.done' | translate }}
</ion-button>
</ion-content>

View File

@ -0,0 +1,156 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { AddonCalendar, AddonCalendarReminderUnits, AddonCalendarValueAndUnit } from '@addons/calendar/services/calendar';
import { Component, Input, OnInit } from '@angular/core';
import { CoreDomUtils } from '@services/utils/dom';
import { ModalController } from '@singletons';
/**
* Modal to choose a reminder time.
*/
@Component({
selector: 'addon-calendar-new-reminder-modal',
templateUrl: 'reminder-time-modal.html',
})
export class AddonCalendarReminderTimeModalComponent implements OnInit {
@Input() initialValue?: AddonCalendarValueAndUnit;
@Input() allowDisable?: boolean;
radioValue = '5m';
customValue = '10';
customUnits = AddonCalendarReminderUnits.MINUTE;
presetOptions = [
{
radioValue: '5m',
value: 5,
unit: AddonCalendarReminderUnits.MINUTE,
label: '',
},
{
radioValue: '10m',
value: 10,
unit: AddonCalendarReminderUnits.MINUTE,
label: '',
},
{
radioValue: '30m',
value: 30,
unit: AddonCalendarReminderUnits.MINUTE,
label: '',
},
{
radioValue: '1h',
value: 1,
unit: AddonCalendarReminderUnits.HOUR,
label: '',
},
{
radioValue: '12h',
value: 12,
unit: AddonCalendarReminderUnits.HOUR,
label: '',
},
{
radioValue: '1d',
value: 1,
unit: AddonCalendarReminderUnits.DAY,
label: '',
},
];
customUnitsOptions = [
{
value: AddonCalendarReminderUnits.MINUTE,
label: 'core.minutes',
},
{
value: AddonCalendarReminderUnits.HOUR,
label: 'core.hours',
},
{
value: AddonCalendarReminderUnits.DAY,
label: 'core.days',
},
{
value: AddonCalendarReminderUnits.WEEK,
label: 'core.weeks',
},
];
/**
* @inheritdoc
*/
async ngOnInit(): Promise<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);
}
}
}

View File

@ -41,6 +41,7 @@
"noevents": "There are no events",
"nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event.",
"reminders": "Reminders",
"units": "Units",
"repeatedevents": "Repeated events",
"repeateditall": "Also apply changes to the other {{$a}} events in this repeat series",
"repeateditthis": "Apply changes to this event only",
@ -54,6 +55,7 @@
"sunday": "Sunday",
"thu": "Thu",
"thursday": "Thursday",
"timebefore": "{{value}} {{units}} before",
"today": "Today",
"tomorrow": "Tomorrow",
"tue": "Tue",
@ -73,4 +75,4 @@
"wednesday": "Wednesday",
"when": "When",
"yesterday": "Yesterday"
}
}

View File

@ -123,34 +123,26 @@
</ion-label>
</ion-item>
<ng-container *ngFor="let reminder of reminders">
<ion-item *ngIf="reminder.time > 0 || defaultTime > 0" class="ion-text-wrap"
[class.item-dimmed]="(reminder.time == -1 ? (event.timestart - defaultTime) : reminder.time) <= currentTime!">
<ion-item *ngIf="reminder.timestamp > 0" class="ion-text-wrap"
[class.item-dimmed]="reminder.timestamp <= currentTime">
<ion-label>
<p *ngIf="reminder.time == -1">
{{ 'core.defaultvalue' | translate :{$a: ((event.timestart - defaultTime) * 1000) | coreFormatDate } }}
</p>
<p *ngIf="reminder.time > 0">{{ reminder.time * 1000 | coreFormatDate }}</p>
<p>{{ reminder.label }}</p>
</ion-label>
<ion-button fill="clear" (click)="cancelNotification(reminder.id, $event)"
[attr.aria-label]="'core.delete' | translate" slot="end"
*ngIf="(reminder.time == -1 ? (event.timestart - defaultTime) : reminder.time) > currentTime!">
[attr.aria-label]="'core.delete' | translate" slot="end" *ngIf="reminder.timestamp > currentTime">
<ion-icon name="fas-trash" color="danger" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-item>
</ng-container>
<ng-container *ngIf="event.timestart + event.timeduration > currentTime!">
<ng-container *ngIf="event.timestart > currentTime">
<ion-item>
<ion-label>
<ion-button expand="block" color="primary" (click)="notificationPicker.open()">
<ion-button expand="block" color="primary" (click)="addReminder()">
{{ 'addon.calendar.setnewreminder' | translate }}
</ion-button>
</ion-label>
</ion-item>
<ion-datetime #notificationPicker hidden [(ngModel)]="notificationTimeText"
[displayFormat]="notificationFormat" [min]="notificationMin" [max]="notificationMax"
[doneText]="'core.add' | translate" (ionChange)="addNotificationTime()" [monthNames]="monthNames">
</ion-datetime>
</ng-container>
</ion-card>
</core-loading>

View File

@ -20,7 +20,7 @@ import {
AddonCalendarEventToDisplay,
AddonCalendarProvider,
} from '../../services/calendar';
import { AddonCalendarHelper } from '../../services/calendar-helper';
import { AddonCalendarEventReminder, AddonCalendarHelper } from '../../services/calendar-helper';
import { AddonCalendarOffline } from '../../services/calendar-offline';
import { AddonCalendarSync, AddonCalendarSyncEvents, AddonCalendarSyncProvider } from '../../services/calendar-sync';
import { CoreApp } from '@services/app';
@ -36,10 +36,9 @@ import { Network, NgZone, Translate } from '@singletons';
import { Subscription } from 'rxjs';
import { CoreNavigator } from '@services/navigator';
import { CoreUtils } from '@services/utils/utils';
import { AddonCalendarReminderDBRecord } from '../../services/database/calendar';
import { ActivatedRoute } from '@angular/router';
import { CoreConstants } from '@/core/constants';
import { CoreLang } from '@services/lang';
import { AddonCalendarReminderTimeModalComponent } from '@addons/calendar/components/reminder-time-modal/reminder-time-modal';
/**
* Page that displays a single calendar event.
@ -57,13 +56,11 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
protected syncObserver: CoreEventObserver;
protected manualSyncObserver: CoreEventObserver;
protected onlineObserver: Subscription;
protected defaultTimeChangedObserver: CoreEventObserver;
protected currentSiteId: string;
protected updateCurrentTime?: number;
eventLoaded = false;
notificationFormat?: string;
notificationMin?: string;
notificationMax?: string;
notificationTimeText?: string;
event?: AddonCalendarEventToDisplay;
courseId?: number;
courseName = '';
@ -72,19 +69,16 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
notificationsEnabled = false;
moduleUrl = '';
categoryPath = '';
currentTime?: number;
defaultTime = 0;
reminders: AddonCalendarReminderDBRecord[] = [];
currentTime = -1;
reminders: AddonCalendarEventReminder[] = [];
canEdit = false;
hasOffline = false;
isOnline = false;
syncIcon = CoreConstants.ICON_LOADING; // Sync icon.
monthNames?: string[];
constructor(
protected route: ActivatedRoute,
) {
this.notificationsEnabled = CoreLocalNotifications.isAvailable();
this.siteHomeId = CoreSites.getCurrentSiteHomeId();
this.currentSiteId = CoreSites.getCurrentSiteId();
@ -121,21 +115,35 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
this.isOnline = CoreApp.isOnline();
});
});
// Reload reminders if default notification time changes.
this.defaultTimeChangedObserver = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
this.loadReminders();
if (this.event) {
AddonCalendar.scheduleEventsNotifications([this.event]);
}
}, this.currentSiteId);
// Set and update current time. Use a 5 seconds error margin.
this.currentTime = CoreTimeUtils.timestamp();
this.updateCurrentTime = window.setInterval(() => {
this.currentTime = CoreTimeUtils.timestamp();
}, 5000);
}
protected async initReminders(): Promise<void> {
if (this.notificationsEnabled) {
this.monthNames = CoreLang.getMonthNames();
this.reminders = await AddonCalendar.getEventReminders(this.eventId);
this.defaultTime = await AddonCalendar.getDefaultNotificationTime() * 60;
// Calculate format to use.
this.notificationFormat =
CoreTimeUtils.fixFormatForDatetime(CoreTimeUtils.convertPHPToMoment(
Translate.instant('core.strftimedatetime'),
));
/**
* Load reminders.
*
* @return Promise resolved when done.
*/
protected async loadReminders(): Promise<void> {
if (!this.notificationsEnabled || !this.event) {
return;
}
const reminders = await AddonCalendar.getEventReminders(this.eventId, this.currentSiteId);
this.reminders = await AddonCalendarHelper.formatReminders(reminders, this.event.timestart, this.currentSiteId);
}
/**
@ -155,7 +163,6 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
this.syncIcon = CoreConstants.ICON_LOADING;
this.fetchEvent();
this.initReminders();
}
/**
@ -209,6 +216,10 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
const event = await AddonCalendar.getEventById(this.eventId);
this.event = await AddonCalendarHelper.formatEventData(event);
// Load reminders, and re-schedule them if needed (maybe the event time has changed).
this.loadReminders();
AddonCalendar.scheduleEventsNotifications([this.event]);
try {
const offlineEvent = AddonCalendarHelper.formatOfflineEventData(
await AddonCalendarOffline.getEvent(this.eventId),
@ -223,29 +234,21 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
this.hasOffline = false;
}
this.currentTime = CoreTimeUtils.timestamp();
this.notificationMin = CoreTimeUtils.userDate(this.currentTime * 1000, 'YYYY-MM-DDTHH:mm', false);
this.notificationMax = CoreTimeUtils.userDate(
(this.event!.timestart + this.event!.timeduration) * 1000,
'YYYY-MM-DDTHH:mm',
false,
);
// Reset some of the calculated data.
this.categoryPath = '';
this.courseName = '';
this.courseUrl = '';
this.moduleUrl = '';
if (this.event!.moduleIcon) {
if (this.event.moduleIcon) {
// It's a module event, translate the module name to the current language.
const name = CoreCourse.translateModuleName(this.event!.modulename || '');
const name = CoreCourse.translateModuleName(this.event.modulename || '');
if (name.indexOf('core.mod_') === -1) {
this.event!.modulename = name;
this.event.modulename = name;
}
// Get the module URL.
this.moduleUrl = this.event!.url || '';
this.moduleUrl = this.event.url || '';
}
const promises: Promise<void>[] = [];
@ -310,22 +313,23 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
/**
* Add a reminder for this event.
*/
async addNotificationTime(): Promise<void> {
if (this.notificationTimeText && this.event && this.event.id) {
let notificationTime = CoreTimeUtils.convertToTimestamp(this.notificationTimeText);
const currentTime = CoreTimeUtils.timestamp();
const minute = Math.floor(currentTime / 60) * 60;
// Check if the notification time is in the same minute as we are, so the notification is triggered.
if (notificationTime >= minute && notificationTime < minute + 60) {
notificationTime = currentTime + 1;
}
await AddonCalendar.addEventReminder(this.event, notificationTime);
this.reminders = await AddonCalendar.getEventReminders(this.eventId);
this.notificationTimeText = undefined;
async addReminder(): Promise<void> {
if (!this.event || !this.event.id) {
return;
}
const reminderTime = await CoreDomUtils.openModal<number>({
component: AddonCalendarReminderTimeModalComponent,
});
if (reminderTime === undefined) {
// User canceled.
return;
}
await AddonCalendar.addEventReminder(this.event, reminderTime, this.currentSiteId);
await this.loadReminders();
}
/**
@ -345,7 +349,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
try {
await AddonCalendar.deleteEventReminder(id);
this.reminders = await AddonCalendar.getEventReminders(this.eventId);
await this.loadReminders();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error deleting reminder');
} finally {
@ -549,6 +553,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
this.syncObserver?.off();
this.manualSyncObserver?.off();
this.onlineObserver?.unsubscribe();
clearInterval(this.updateCurrentTime);
}
}

View File

@ -8,18 +8,10 @@
</ion-header>
<ion-content>
<ion-list>
<ion-item *ngIf="defaultTime != -1">
<ion-item *ngIf="defaultTimeLabel">
<ion-label>{{ 'addon.calendar.defaultnotificationtime' | translate }}</ion-label>
<ion-select [(ngModel)]="defaultTime" (ionChange)="updateDefaultTime(defaultTime)" interface="action-sheet"
[interfaceOptions]="{header: 'addon.calendar.defaultnotificationtime' | translate}">
<ion-select-option [value]="0">{{ 'core.settings.disabled' | translate }}</ion-select-option>
<ion-select-option [value]="10">{{ 600 | coreDuration }}</ion-select-option>
<ion-select-option [value]="30">{{ 1800 | coreDuration }}</ion-select-option>
<ion-select-option [value]="60">{{ 3600 | coreDuration }}</ion-select-option>
<ion-select-option [value]="120">{{ 7200 | coreDuration }}</ion-select-option>
<ion-select-option [value]="360">{{ 21600 | coreDuration }}</ion-select-option>
<ion-select-option [value]="720">{{ 43200 | coreDuration }}</ion-select-option>
<ion-select-option [value]="1440">{{ 86400 | coreDuration }}</ion-select-option>
<ion-select [(ngModel)]="defaultTimeLabel" (click)="changeDefaultTime($event)">
<ion-select-option [value]="defaultTimeLabel">{{ defaultTimeLabel }}</ion-select-option>
</ion-select>
</ion-item>
</ion-list>

View File

@ -13,9 +13,16 @@
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { AddonCalendar, AddonCalendarProvider } from '../../services/calendar';
import {
AddonCalendar,
AddonCalendarProvider,
AddonCalendarReminderUnits,
AddonCalendarValueAndUnit,
} from '../../services/calendar';
import { CoreEvents } from '@singletons/events';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { AddonCalendarReminderTimeModalComponent } from '@addons/calendar/components/reminder-time-modal/reminder-time-modal';
/**
* Page that displays the calendar settings.
@ -26,13 +33,51 @@ import { CoreSites } from '@services/sites';
})
export class AddonCalendarSettingsPage implements OnInit {
defaultTime = -1;
defaultTimeLabel = '';
protected defaultTime: AddonCalendarValueAndUnit = {
value: 0,
unit: AddonCalendarReminderUnits.MINUTE,
};
/**
* View loaded.
*/
async ngOnInit(): Promise<void> {
this.defaultTime = await AddonCalendar.getDefaultNotificationTime();
const defaultTime = await AddonCalendar.getDefaultNotificationTime();
this.defaultTime = AddonCalendarProvider.convertSecondsToValueAndUnit(defaultTime);
this.defaultTimeLabel = AddonCalendar.getUnitValueLabel(this.defaultTime.value, this.defaultTime.unit);
}
/**
* Change default time.
*
* @param e Event.
* @return Promise resolved when done.
*/
async changeDefaultTime(e: Event): Promise<void> {
e.stopPropagation();
e.stopImmediatePropagation();
e.preventDefault();
const reminderTime = await CoreDomUtils.openModal<number>({
component: AddonCalendarReminderTimeModalComponent,
componentProps: {
initialValue: this.defaultTime,
allowDisable: true,
},
});
if (reminderTime === undefined) {
// User canceled.
return;
}
this.defaultTime = AddonCalendarProvider.convertSecondsToValueAndUnit(reminderTime);
this.defaultTimeLabel = AddonCalendar.getUnitValueLabel(this.defaultTime.value, this.defaultTime.unit);
this.updateDefaultTime(reminderTime);
}
/**

View File

@ -23,6 +23,7 @@ import {
AddonCalendarEventType,
AddonCalendarGetEventsEvent,
AddonCalendarProvider,
AddonCalendarReminderUnits,
AddonCalendarWeek,
AddonCalendarWeekDay,
} from './calendar';
@ -35,6 +36,7 @@ import { makeSingleton } from '@singletons';
import { AddonCalendarSyncInvalidateEvent } from './calendar-sync';
import { AddonCalendarOfflineEventDBRecord } from './database/calendar-offline';
import { CoreCategoryData } from '@features/courses/services/courses';
import { AddonCalendarReminderDBRecord } from './database/calendar';
/**
* Context levels enumeration.
@ -282,6 +284,55 @@ export class AddonCalendarHelperProvider {
}
}
/**
* Format reminders, adding calculated data.
*
* @param reminders Reminders.
* @param timestart Event timestart.
* @param siteId Site ID.
* @return Formatted reminders.
*/
async formatReminders(
reminders: AddonCalendarReminderDBRecord[],
timestart: number,
siteId?: string,
): Promise<AddonCalendarEventReminder[]> {
const defaultTime = await AddonCalendar.getDefaultNotificationTime(siteId);
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) => {
if (reminder.time === null) {
// Default time. Check if default notifications are disabled.
if (defaultTimeValue !== undefined && defaultTimeUnit) {
reminder.value = defaultTimeValue;
reminder.unit = defaultTimeUnit;
reminder.timestamp = eventTimestart - reminder.value * reminder.unit;
}
} else {
const data = AddonCalendarProvider.convertSecondsToValueAndUnit(reminder.time);
reminder.value = data.value;
reminder.unit = data.unit;
reminder.timestamp = eventTimestart - reminder.time;
}
if (reminder.value && reminder.unit) {
reminder.label = AddonCalendar.getUnitValueLabel(reminder.value, reminder.unit, reminder.time === null);
}
return reminder;
});
}
/**
* Get options (name & value) for each allowed event type.
*
@ -725,3 +776,13 @@ export type AddonCalendarEventTypeOption = {
name: string;
value: AddonCalendarEventType;
};
/**
* Formatted event reminder.
*/
export type AddonCalendarEventReminder = AddonCalendarReminderDBRecord & {
value?: number; // Amount of time.
unit?: AddonCalendarReminderUnits; // Units.
timestamp?: number; // Timestamp (in seconds).
label?: string; // Label to represent the reminder.
};

View File

@ -53,6 +53,16 @@ export enum AddonCalendarEventType {
USER = 'user',
}
/**
* Units to set a reminder.
*/
export enum AddonCalendarReminderUnits {
MINUTE = CoreConstants.SECONDS_MINUTE,
HOUR = CoreConstants.SECONDS_HOUR,
DAY = CoreConstants.SECONDS_DAY,
WEEK = CoreConstants.SECONDS_WEEK,
}
declare module '@singletons/events' {
/**
@ -72,6 +82,21 @@ declare module '@singletons/events' {
}
const REMINDER_UNITS_LABELS = {
single: {
[AddonCalendarReminderUnits.MINUTE]: 'core.minute',
[AddonCalendarReminderUnits.HOUR]: 'core.hour',
[AddonCalendarReminderUnits.DAY]: 'core.day',
[AddonCalendarReminderUnits.WEEK]: 'core.week',
},
multi: {
[AddonCalendarReminderUnits.MINUTE]: 'core.minutes',
[AddonCalendarReminderUnits.HOUR]: 'core.hours',
[AddonCalendarReminderUnits.DAY]: 'core.days',
[AddonCalendarReminderUnits.WEEK]: 'core.weeks',
},
};
/**
* Service to handle calendar events.
*/
@ -82,7 +107,7 @@ export class AddonCalendarProvider {
static readonly COMPONENT = 'AddonCalendarEvents';
static readonly DEFAULT_NOTIFICATION_TIME_CHANGED = 'AddonCalendarDefaultNotificationTimeChangedEvent';
static readonly DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime';
static readonly DEFAULT_NOTIFICATION_TIME = 60;
static readonly DEFAULT_NOTIFICATION_TIME = 3600;
static readonly STARTING_WEEK_DAY = 'addon_calendar_starting_week_day';
static readonly NEW_EVENT_EVENT = 'addon_calendar_new_event';
static readonly NEW_EVENT_DISCARDED_EVENT = 'addon_calendar_new_event_discarded';
@ -156,6 +181,41 @@ export class AddonCalendarProvider {
return !!site?.isVersionGreaterEqualThan('3.7.1');
}
/**
* Given a number of seconds, convert it to a unit&value format compatible with reminders.
*
* @param seconds Number of seconds.
* @return Value and unit.
*/
static convertSecondsToValueAndUnit(seconds: number): AddonCalendarValueAndUnit {
if (seconds <= 0) {
return {
value: 0,
unit: AddonCalendarReminderUnits.MINUTE,
};
} else if (seconds % AddonCalendarReminderUnits.WEEK === 0) {
return {
value: seconds / AddonCalendarReminderUnits.WEEK,
unit: AddonCalendarReminderUnits.WEEK,
};
} else if (seconds % AddonCalendarReminderUnits.DAY === 0) {
return {
value: seconds / AddonCalendarReminderUnits.DAY,
unit: AddonCalendarReminderUnits.DAY,
};
} else if (seconds % AddonCalendarReminderUnits.HOUR === 0) {
return {
value: seconds / AddonCalendarReminderUnits.HOUR,
unit: AddonCalendarReminderUnits.HOUR,
};
} else {
return {
value: seconds / AddonCalendarReminderUnits.MINUTE,
unit: AddonCalendarReminderUnits.MINUTE,
};
}
}
/**
* Delete an event.
*
@ -248,7 +308,7 @@ export class AddonCalendarProvider {
REMINDERS_TABLE,
{ eventid: eventId },
).then((reminders) =>
Promise.all(reminders.map((reminder) => this.deleteEventReminder(reminder.id!, siteId)))));
Promise.all(reminders.map((reminder) => this.deleteEventReminder(reminder.id, siteId)))));
try {
await Promise.all(promises);
@ -548,7 +608,7 @@ export class AddonCalendarProvider {
* Get the configured default notification time.
*
* @param siteId ID of the site. If not defined, use current site.
* @return Promise resolved with the default time.
* @return Promise resolved with the default time (in seconds).
*/
async getDefaultNotificationTime(siteId?: string): Promise<number> {
siteId = siteId || CoreSites.getCurrentSiteId();
@ -678,24 +738,27 @@ export class AddonCalendarProvider {
/**
* Adds an event reminder and schedule a new notification.
*
* @param event Event to update its notification time.
* @param time New notification setting timestamp.
* @param event Event to set the reminder.
* @param time Amount of seconds of the reminder. Undefined for default reminder.
* @param siteId ID of the site the event belongs to. If not defined, use current site.
* @return Promise resolved when the notification is updated.
*/
async addEventReminder(
event: { id: number; timestart: number; timeduration: number; name: string},
time: number,
time?: number | null,
siteId?: string,
): Promise<void> {
const site = await CoreSites.getSite(siteId);
const reminder: AddonCalendarReminderDBRecord = {
const reminder: Partial<AddonCalendarReminderDBRecord> = {
eventid: event.id,
time: time,
time: time ?? null,
};
const reminderId = await site.getDb().insertRecord(REMINDERS_TABLE, reminder);
await this.scheduleEventNotification(event, reminderId, time, site.getId());
const timestamp = time ? event.timestart - time : time;
await this.scheduleEventNotification(event, reminderId, timestamp, site.getId());
}
/**
@ -1022,6 +1085,35 @@ export class AddonCalendarProvider {
(categoryId ? categoryId : '');
}
/**
* Given a value and a unit, return the translated label.
*
* @param value Value.
* @param unit Unit.
* @param addDefaultLabel Whether to add the "Default" text.
* @return Translated label.
*/
getUnitValueLabel(value: number, unit: AddonCalendarReminderUnits, addDefaultLabel = false): string {
if (value === 0) {
return Translate.instant('core.settings.disabled');
}
const unitsLabel = value === 1 ?
REMINDER_UNITS_LABELS.single[unit] :
REMINDER_UNITS_LABELS.multi[unit];
const label = Translate.instant('addon.calendar.timebefore', {
units: Translate.instant(unitsLabel),
value: value,
});
if (addDefaultLabel) {
return Translate.instant('core.defaultvalue', { $a: label });
}
return label;
}
/**
* Get upcoming calendar events.
*
@ -1335,14 +1427,14 @@ export class AddonCalendarProvider {
*
* @param event Event to schedule.
* @param reminderId The reminder ID.
* @param time Notification setting time (in minutes). E.g. 10 means "notificate 10 minutes before start".
* @param time Notification timestamp (in seconds). Undefined for default time.
* @param siteId Site ID the event belongs to. If not defined, use current site.
* @return Promise resolved when the notification is scheduled.
*/
protected async scheduleEventNotification(
event: { id: number; timestart: number; name: string},
reminderId: number,
time: number,
time?: number | null,
siteId?: string,
): Promise<void> {
@ -1357,16 +1449,16 @@ export class AddonCalendarProvider {
return CoreLocalNotifications.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId);
}
if (time == -1) {
// If time is -1, get event default time to calculate the notification time.
if (!time) {
// Get event default time to calculate the notification time.
time = await this.getDefaultNotificationTime(siteId);
if (time == 0) {
if (time === 0) {
// Default notification time is disabled, do not show.
return CoreLocalNotifications.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId);
}
time = event.timestart - (time * 60);
time = event.timestart - time;
}
time = time * 1000;
@ -1417,17 +1509,18 @@ export class AddonCalendarProvider {
siteId = siteId || CoreSites.getCurrentSiteId();
const promises = events.map(async (event) => {
const timeEnd = (event.timestart + event.timeduration) * 1000;
if (timeEnd <= new Date().getTime()) {
// The event has finished already, don't schedule it.
if (event.timestart * 1000 <= Date.now()) {
// The event has already started, don't schedule it.
return this.deleteLocalEvent(event.id, siteId);
}
const reminders = await this.getEventReminders(event.id, siteId);
const p2 = reminders.map((reminder: AddonCalendarReminderDBRecord) =>
this.scheduleEventNotification(event, (reminder.id!), reminder.time, siteId));
const p2 = reminders.map((reminder) => {
const time = reminder.time ? event.timestart - reminder.time : reminder.time;
return this.scheduleEventNotification(event, reminder.id, time, siteId);
});
await Promise.all(p2);
});
@ -1467,7 +1560,8 @@ export class AddonCalendarProvider {
const reminders = await this.getEventReminders(event.id, siteId);
if (reminders.length == 0) {
this.addEventReminder(event, -1, siteId);
// No reminders, create the default one.
this.addEventReminder(event, undefined, siteId);
}
}
@ -1537,9 +1631,7 @@ export class AddonCalendarProvider {
const site = await CoreSites.getSite(siteId);
siteId = site.getId();
await Promise.all(events.map((event: AddonCalendarGetEventsEvent| AddonCalendarCalendarEvent) =>
// If event does not exist on the DB, schedule the reminder.
this.storeEventInLocalDb(event, siteId)));
await Promise.all(events.map((event) => this.storeEventInLocalDb(event, siteId)));
}
/**
@ -2154,3 +2246,11 @@ type AddonCalendarPushNotificationData = {
reminderId: number;
siteId: string;
};
/**
* Value and unit for reminders.
*/
export type AddonCalendarValueAndUnit = {
value: number;
unit: AddonCalendarReminderUnits;
};

View File

@ -14,7 +14,8 @@
import { SQLiteDB } from '@classes/sqlitedb';
import { CoreSiteSchema } from '@services/sites';
import { AddonCalendarEventType } from '../calendar';
import { CoreUtils } from '@services/utils/utils';
import { AddonCalendar, AddonCalendarEventType } from '../calendar';
/**
* Database variables for AddonDatabase service.
@ -23,7 +24,7 @@ export const EVENTS_TABLE = 'addon_calendar_events_3';
export const REMINDERS_TABLE = 'addon_calendar_reminders';
export const CALENDAR_SITE_SCHEMA: CoreSiteSchema = {
name: 'AddonCalendarProvider',
version: 3,
version: 4,
canBeCleared: [EVENTS_TABLE],
tables: [
{
@ -199,8 +200,9 @@ export const CALENDAR_SITE_SCHEMA: CoreSiteSchema = {
],
},
],
async migrate(db: SQLiteDB, oldVersion: number): Promise<void> {
async migrate(db: SQLiteDB, oldVersion: number, siteId: string): Promise<void> {
if (oldVersion < 3) {
// Migrate calendar events. New format @since 3.7.
let oldTable = 'addon_calendar_events_2';
try {
@ -212,6 +214,46 @@ export const CALENDAR_SITE_SCHEMA: CoreSiteSchema = {
await db.migrateTable(oldTable, EVENTS_TABLE);
}
if (oldVersion < 4) {
// Migrate reminders. New format @since 4.0.
const defaultTime = await CoreUtils.ignoreErrors(AddonCalendar.getDefaultNotificationTime(siteId));
if (defaultTime) {
// Convert from minutes to seconds.
AddonCalendar.setDefaultNotificationTime(defaultTime * 60, siteId);
}
const records = await db.getAllRecords<AddonCalendarReminderDBRecord>(REMINDERS_TABLE);
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. Delete the reminder.
await db.deleteRecords(REMINDERS_TABLE, { id: record.id });
return;
}
}
if (!record.time || record.time === -1) {
// Default reminder. Use null now.
record.time = null;
} else if (record.time > events[record.eventid].timestart) {
// Reminder is after the event, delete it.
await db.deleteRecords(REMINDERS_TABLE, { id: record.id });
return;
} else {
record.time = events[record.eventid].timestart - record.time;
}
return this.insertRecord(REMINDERS_TABLE, record);
}));
}
},
};
@ -257,7 +299,7 @@ export type AddonCalendarEventDBRecord = {
};
export type AddonCalendarReminderDBRecord = {
id?: number;
id: number;
eventid: number;
time: number;
time: number | null; // Number of seconds before the event, null for default time.
};

View File

@ -60,6 +60,7 @@
"coursedetails": "Course details",
"coursenogroups": "You are not a member of any group of this course.",
"currentdevice": "Current device",
"custom": "Custom",
"datastoredoffline": "Data stored in the device because it couldn't be sent. It will be sent automatically later.",
"date": "Date",
"day": "day",
@ -157,6 +158,8 @@
"maxsizeandattachments": "Maximum file size: {{$a.size}}, maximum number of files: {{$a.attachments}}",
"min": "min",
"mins": "mins",
"minute": "minute",
"minutes": "minutes",
"misc": "Miscellaneous",
"mod_assign": "Assignment",
"mod_assignment": "Assignment 2.2 (Disabled)",
@ -330,6 +333,8 @@
"viewprofile": "View profile",
"warningofflinedatadeleted": "Offline data from {{component}} '{{name}}' has been deleted. {{error}}",
"warnopeninbrowser": "<p>You are about to leave the app to open the following URL in your device's browser. Do you want to continue?</p>\n<p><b>{{url}}</b></p>",
"week": "week",
"weeks": "weeks",
"whatisyourage": "What is your age?",
"wheredoyoulive": "In which country do you live?",
"whoissiteadmin": "\"Site Administrators\" are the people who manage the Moodle at your school/university/company or learning organisation. If you don't know how to contact them, please contact your teachers/trainers.",