Merge pull request #3001 from dpalou/MOBILE-3909

Mobile 3909
main
Pau Ferrer Ocaña 2021-11-22 14:55:39 +01:00 committed by GitHub
commit 6703b89b94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1176 additions and 454 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",
@ -1944,6 +1947,8 @@
"core.maxsizeandattachments": "moodle",
"core.min": "moodle",
"core.mins": "moodle",
"core.minute": "moodle",
"core.minutes": "moodle",
"core.misc": "admin",
"core.mod_assign": "assign/pluginname",
"core.mod_assignment": "assignment/pluginname",
@ -2284,6 +2289,8 @@
"core.viewprofile": "moodle",
"core.warningofflinedatadeleted": "local_moodlemobileapp",
"core.warnopeninbrowser": "local_moodlemobileapp",
"core.week": "moodle",
"core.weeks": "moodle",
"core.whatisyourage": "moodle",
"core.wheredoyoulive": "moodle",
"core.whoissiteadmin": "local_moodlemobileapp",

View File

@ -94,7 +94,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
this.obsDefaultTimeChange = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
this.weeks.forEach((week) => {
week.days.forEach((day) => {
AddonCalendar.scheduleEventsNotifications(day.eventsFormated!);
AddonCalendar.scheduleEventsNotifications(day.eventsFormated || []);
});
});
}, this.currentSiteId);
@ -150,7 +150,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
if (this.weeks) {
// Check if there's any change in the filter object.
const changes = this.differ.diff(this.filter!);
const changes = this.differ.diff(this.filter || {});
if (changes) {
this.filterEvents();
}
@ -173,8 +173,8 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
this.offlineEvents = AddonCalendarHelper.classifyIntoMonths(events);
// Get the IDs of events edited in offline.
const filtered = events.filter((event) => event.id! > 0);
this.offlineEditedEventsIds = filtered.map((event) => event.id!);
const filtered = events.filter((event) => event.id > 0);
this.offlineEditedEventsIds = filtered.map((event) => event.id);
return;
}));
@ -261,7 +261,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
isPast = day.ispast;
if (day.istoday) {
day.eventsFormated!.forEach((event) => {
day.eventsFormated?.forEach((event) => {
event.ispast = this.isEventPast(event);
});
}
@ -306,8 +306,8 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
this.weeks.forEach((week) => {
week.days.forEach((day) => {
day.filteredEvents = AddonCalendarHelper.getFilteredEvents(
day.eventsFormated!,
this.filter!,
day.eventsFormated || [],
this.filter,
this.categories,
);
@ -466,14 +466,14 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
week.days.forEach((day) => {
// Schedule notifications for the events retrieved (only future events will be scheduled).
AddonCalendar.scheduleEventsNotifications(day.eventsFormated!);
AddonCalendar.scheduleEventsNotifications(day.eventsFormated || []);
if (monthOfflineEvents || this.deletedEvents.length) {
// There is offline data, merge it.
if (this.deletedEvents.length) {
// Mark as deleted the events that were deleted in offline.
day.eventsFormated!.forEach((event) => {
day.eventsFormated?.forEach((event) => {
event.deleted = this.deletedEvents.indexOf(event.id) != -1;
});
}
@ -483,10 +483,10 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
day.events = day.events.filter((event) => this.offlineEditedEventsIds.indexOf(event.id) == -1);
}
if (monthOfflineEvents && monthOfflineEvents[day.mday]) {
if (monthOfflineEvents && monthOfflineEvents[day.mday] && day.eventsFormated) {
// Add the offline events (either new or edited).
day.eventsFormated =
AddonCalendarHelper.sortEvents(day.eventsFormated!.concat(monthOfflineEvents[day.mday]));
AddonCalendarHelper.sortEvents(day.eventsFormated.concat(monthOfflineEvents[day.mday]));
}
}
});
@ -505,7 +505,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
this.weeks.forEach((week) => {
week.days.forEach((day) => {
const event = day.eventsFormated!.find((event) => event.id == eventId);
const event = day.eventsFormated?.find((event) => event.id == eventId);
if (event) {
event.deleted = false;
@ -521,7 +521,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
* @return True if it's in the past.
*/
protected isEventPast(event: { timestart: number; timeduration: number}): boolean {
return (event.timestart + event.timeduration) < this.currentTime!;
return (event.timestart + event.timeduration) < (this.currentTime || CoreTimeUtils.timestamp());
}
/**

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

@ -106,7 +106,7 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On
*/
ngDoCheck(): void {
// Check if there's any change in the filter object.
const changes = this.differ.diff(this.filter!);
const changes = this.differ.diff(this.filter || {});
if (changes) {
this.filterEvents();
}
@ -183,7 +183,7 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On
// Re-calculate the formatted time so it uses the device date.
const promises = this.events.map((event) =>
AddonCalendar.formatEventTime(event, this.timeFormat!).then((time) => {
AddonCalendar.formatEventTime(event, this.timeFormat).then((time) => {
event.formattedtime = time;
return;
@ -221,7 +221,7 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On
* Filter events based on the filter popover.
*/
protected filterEvents(): void {
this.filteredEvents = AddonCalendarHelper.getFilteredEvents(this.events, this.filter!, this.categories);
this.filteredEvents = AddonCalendarHelper.getFilteredEvents(this.events, this.filter, this.categories);
}
/**

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

@ -286,7 +286,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
this.offlineEvents = AddonCalendarHelper.classifyIntoMonths(offlineEvents);
// Get the IDs of events edited in offline.
this.offlineEditedEventsIds = offlineEvents.filter((event) => event.id! > 0).map((event) => event.id!);
this.offlineEditedEventsIds = offlineEvents.filter((event) => event.id > 0).map((event) => event.id);
return;
}));
@ -361,7 +361,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
const promises = this.events.map((event) => {
event.ispast = this.isPastDay || (this.isCurrentDay && this.isEventPast(event));
return AddonCalendar.formatEventTime(event, this.timeFormat!, true, dayTime).then((time) => {
return AddonCalendar.formatEventTime(event, this.timeFormat, true, dayTime).then((time) => {
event.formattedtime = time;
return;
@ -521,12 +521,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
* @param eventId Event to load.
*/
gotoEvent(eventId: number): void {
if (eventId < 0) {
// It's an offline event, go to the edit page.
this.openEdit(eventId);
} else {
CoreNavigator.navigateToSitePath(`/calendar/event/${eventId}`);
}
CoreNavigator.navigateToSitePath(`/calendar/event/${eventId}`);
}
/**

View File

@ -113,120 +113,129 @@
</ion-item>
</ng-container>
<!-- Advanced options. -->
<ion-item button class="ion-text-wrap divider" (click)="toggleAdvanced()" detail="false">
<ion-icon *ngIf="!advanced" name="fas-caret-right" flip-rtl slot="start" aria-hidden="true"></ion-icon>
<ion-icon *ngIf="advanced" name="fas-caret-down" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading" *ngIf="!advanced">{{ 'core.showmore' | translate }}</p>
<p class="item-heading" *ngIf="advanced">{{ 'core.showless' | translate }}</p>
</ion-label>
</ion-item>
<div [hidden]="!advanced">
<!-- Description. -->
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<p class="item-heading">{{ 'core.description' | translate }}</p>
<!-- Reminders. Right now, only allow adding them here for new events. -->
<ng-container *ngIf="notificationsEnabled && !eventId">
<ion-item-divider class="addon-calendar-reminders-title">
<ion-label>
<p class="item-heading">{{ 'addon.calendar.reminders' | translate }}</p>
</ion-label>
<core-rich-text-editor [control]="descriptionControl" [attr.aria-label]="'core.description' | translate"
[placeholder]="'core.description' | translate" name="description" [component]="component"
[componentId]="eventId" [autoSave]="false"></core-rich-text-editor>
<ion-button fill="clear" color="dark" (click)="addReminder()" slot="end"
[attr.aria-label]="'addon.calendar.setnewreminder' | translate">
<ion-icon name="fas-plus" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-item-divider>
<ion-item *ngFor="let reminder of reminders" class="ion-text-wrap">
<ion-label>
<p>{{ reminder.label }}</p>
</ion-label>
<ion-button fill="clear" (click)="removeReminder(reminder)" [attr.aria-label]="'core.delete' | translate"
slot="end">
<ion-icon name="fas-trash" color="danger" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-item>
</ng-container>
<!-- Location. -->
<!-- Duration. -->
<div class="ion-text-wrap addon-calendar-radio-container">
<ion-radio-group formControlName="duration">
<ion-item-divider class="addon-calendar-radio-title">
<ion-label>
<p class="item-heading">{{ 'addon.calendar.eventduration' | translate }}</p>
</ion-label>
</ion-item-divider>
<ion-item>
<ion-label>
<p>{{ 'addon.calendar.durationnone' | translate }}</p>
</ion-label>
<ion-radio slot="end" [value]="0"></ion-radio>
</ion-item>
<ion-item lines="none">
<ion-label>
<p>{{ 'addon.calendar.durationuntil' | translate }}</p>
</ion-label>
<ion-radio slot="end" [value]="1"></ion-radio>
</ion-item>
<ion-item *ngIf="form.controls.duration.value === 1">
<ion-label position="stacked"></ion-label>
<ion-datetime formControlName="timedurationuntil" [max]="maxDate" [min]="minDate"
[placeholder]="'addon.calendar.durationuntil' | translate"
[displayFormat]="dateFormat" display-timezone="utc">
</ion-datetime>
</ion-item>
<ion-item lines="none">
<ion-label>
<p>{{ 'addon.calendar.durationminutes' | translate }}</p>
</ion-label>
<ion-radio slot="end" [value]="2"></ion-radio>
</ion-item>
<ion-item *ngIf="form.controls.duration.value === 2">
<ion-label class="sr-only">{{ 'addon.calendar.durationminutes' | translate }}</ion-label>
<ion-input type="number" name="timedurationminutes" slot="end"
[placeholder]="'addon.calendar.durationminutes' | translate"
formControlName="timedurationminutes"></ion-input>
</ion-item>
</ion-radio-group>
</div>
<!-- Repeat (for new events). -->
<ng-container *ngIf="!eventId || eventId < 0">
<ion-item class="ion-text-wrap divider" lines="none">
<ion-label>
<p class="item-heading">{{ 'addon.calendar.repeatevent' | translate }}</p>
</ion-label>
<ion-checkbox slot="end" formControlName="repeat"></ion-checkbox>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<p class="item-heading">{{ 'core.location' | translate }}</p>
<p>{{ 'addon.calendar.repeatweeksl' | translate }}</p>
</ion-label>
<ion-input type="text" name="location" [placeholder]="'core.location' | translate" formControlName="location">
<ion-input type="number" name="repeats" formControlName="repeats" [disabled]="!form.controls.repeat.value">
</ion-input>
</ion-item>
</ng-container>
<!-- Duration. -->
<div class="ion-text-wrap addon-calendar-radio-container">
<ion-radio-group formControlName="duration">
<ion-item-divider class="addon-calendar-radio-title">
<ion-label>
<p class="item-heading">{{ 'addon.calendar.eventduration' | translate }}</p>
</ion-label>
</ion-item-divider>
<ion-item>
<ion-label>
<p>{{ 'addon.calendar.durationnone' | translate }}</p>
</ion-label>
<ion-radio slot="end" [value]="0"></ion-radio>
</ion-item>
<ion-item lines="none">
<ion-label>
<p>{{ 'addon.calendar.durationuntil' | translate }}</p>
</ion-label>
<ion-radio slot="end" [value]="1"></ion-radio>
</ion-item>
<ion-item>
<ion-label position="stacked"></ion-label>
<ion-datetime formControlName="timedurationuntil" [max]="maxDate" [min]="minDate"
[placeholder]="'addon.calendar.durationuntil' | translate"
[displayFormat]="dateFormat" [disabled]="form.controls.duration.value != 1"
display-timezone="utc">
</ion-datetime>
</ion-item>
<ion-item lines="none">
<ion-label>
<p>{{ 'addon.calendar.durationminutes' | translate }}</p>
</ion-label>
<ion-radio slot="end" [value]="2"></ion-radio>
</ion-item>
<ion-item>
<ion-label class="sr-only">{{ 'addon.calendar.durationminutes' | translate }}</ion-label>
<ion-input type="number" name="timedurationminutes" slot="end"
[placeholder]="'addon.calendar.durationminutes' | translate"
formControlName="timedurationminutes" [disabled]="form.controls.duration.value != 2"></ion-input>
</ion-item>
</ion-radio-group>
</div>
<!-- Repeat (for new events). -->
<ng-container *ngIf="!eventId || eventId < 0">
<ion-item class="ion-text-wrap divider" lines="none">
<!-- Apply to all events or just this one (editing repeated events). -->
<div *ngIf="eventRepeatId" class="ion-text-wrap addon-calendar-radio-container">
<ion-radio-group formControlName="repeateditall">
<ion-item-divider class="addon-calendar-radio-title">
<ion-label>
<p class="item-heading">{{ 'addon.calendar.repeatevent' | translate }}</p>
<p class="item-heading">{{ 'addon.calendar.repeatedevents' | translate }}</p>
</ion-label>
<ion-checkbox slot="end" formControlName="repeat"></ion-checkbox>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<p>{{ 'addon.calendar.repeatweeksl' | translate }}</p>
</ion-item-divider>
<ion-item>
<ion-label>
<p>{{ 'addon.calendar.repeateditall' | translate:{$a: otherEventsCount} }}</p>
</ion-label>
<ion-input type="number" name="repeats" formControlName="repeats" [disabled]="!form.controls.repeat.value">
</ion-input>
<ion-radio slot="end" value="1"></ion-radio>
</ion-item>
</ng-container>
<!-- Apply to all events or just this one (editing repeated events). -->
<div *ngIf="eventRepeatId" class="ion-text-wrap addon-calendar-radio-container">
<ion-radio-group formControlName="repeateditall">
<ion-item-divider class="addon-calendar-radio-title">
<ion-label>
<p class="item-heading">{{ 'addon.calendar.repeatedevents' | translate }}</p>
</ion-label>
</ion-item-divider>
<ion-item>
<ion-label>
<p>{{ 'addon.calendar.repeateditall' | translate:{$a: otherEventsCount} }}</p>
</ion-label>
<ion-radio slot="end" value="1"></ion-radio>
</ion-item>
<ion-item>
<ion-label>
<p>{{ 'addon.calendar.repeateditthis' | translate }}</p>
</ion-label>
<ion-radio slot="end" value="0"></ion-radio>
</ion-item>
</ion-radio-group>
</div>
<ion-item>
<ion-label>
<p>{{ 'addon.calendar.repeateditthis' | translate }}</p>
</ion-label>
<ion-radio slot="end" value="0"></ion-radio>
</ion-item>
</ion-radio-group>
</div>
<!-- Description. -->
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<p class="item-heading">{{ 'core.description' | translate }}</p>
</ion-label>
<core-rich-text-editor [control]="descriptionControl" [attr.aria-label]="'core.description' | translate"
[placeholder]="'core.description' | translate" name="description" [component]="component"
[componentId]="eventId" [autoSave]="false"></core-rich-text-editor>
</ion-item>
<!-- Location. -->
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<p class="item-heading">{{ 'core.location' | translate }}</p>
</ion-label>
<ion-input type="text" name="location" [placeholder]="'core.location' | translate" formControlName="location">
</ion-input>
</ion-item>
<ion-item>
<ion-label>
<ion-row>

View File

@ -33,7 +33,7 @@ import {
AddonCalendarSubmitCreateUpdateFormDataWSParams,
} from '../../services/calendar';
import { AddonCalendarOffline } from '../../services/calendar-offline';
import { AddonCalendarEventTypeOption, AddonCalendarHelper } from '../../services/calendar-helper';
import { AddonCalendarEventReminder, AddonCalendarEventTypeOption, AddonCalendarHelper } from '../../services/calendar-helper';
import { AddonCalendarSync, AddonCalendarSyncProvider } from '../../services/calendar-sync';
import { CoreSite } from '@classes/site';
import { Translate } from '@singletons';
@ -43,6 +43,8 @@ import { CoreError } from '@classes/errors/error';
import { CoreNavigator } from '@services/navigator';
import { CanLeave } from '@guards/can-leave';
import { CoreForms } from '@singletons/form';
import { CoreLocalNotifications } from '@services/local-notifications';
import { AddonCalendarReminderTimeModalComponent } from '@addons/calendar/components/reminder-time-modal/reminder-time-modal';
/**
* Page that displays a form to create/edit an event.
@ -68,7 +70,6 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
groups: CoreGroup[] = [];
loadingGroups = false;
courseGroupSet = false;
advanced = false;
errors: Record<string, string>;
error = false;
eventRepeatId?: number;
@ -83,6 +84,10 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
groupControl: FormControl;
descriptionControl: FormControl;
// Reminders.
notificationsEnabled = false;
reminders: AddonCalendarEventCandidateReminder[] = [];
protected courseId!: number;
protected originalData?: AddonCalendarOfflineEventDBRecord;
protected currentSite: CoreSite;
@ -95,6 +100,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
protected fb: FormBuilder,
) {
this.currentSite = CoreSites.getRequiredCurrentSite();
this.notificationsEnabled = CoreLocalNotifications.isAvailable();
this.errors = {
required: Translate.instant('core.required'),
};
@ -140,6 +146,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
this.form.addControl('timedurationuntil', this.fb.control(currentDate));
this.form.addControl('courseid', this.fb.control(this.courseId));
this.initReminders();
this.fetchData().finally(() => {
this.originalData = CoreUtils.clone(this.form.value);
this.loaded = true;
@ -171,17 +178,19 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
if (this.eventId && !this.gotEventData) {
// Editing an event, get the event data. Wait for sync first.
const eventId = this.eventId;
promises.push(AddonCalendarSync.waitForSync(AddonCalendarSyncProvider.SYNC_ID).then(async () => {
// Do not block if the scope is already destroyed.
if (!this.isDestroyed && this.eventId) {
CoreSync.blockOperation(AddonCalendarProvider.COMPONENT, this.eventId);
CoreSync.blockOperation(AddonCalendarProvider.COMPONENT, eventId);
}
let eventForm: AddonCalendarEvent | AddonCalendarOfflineEventDBRecord | undefined;
// Get the event offline data if there's any.
try {
eventForm = await AddonCalendarOffline.getEvent(this.eventId!);
eventForm = await AddonCalendarOffline.getEvent(eventId);
this.hasOffline = true;
} catch {
@ -189,9 +198,9 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
this.hasOffline = false;
}
if (this.eventId! > 0) {
if (eventId > 0) {
// It's an online event. get its data from server.
const event = await AddonCalendar.getEventById(this.eventId!);
const event = await AddonCalendar.getEventById(eventId);
if (!eventForm) {
eventForm = event; // Use offline data first.
@ -431,13 +440,6 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
}
}
/**
* Show or hide advanced form fields.
*/
toggleAdvanced(): void {
this.advanced = !this.advanced;
}
selectDuration(duration: string): void {
this.form.controls.duration.setValue(duration);
}
@ -517,7 +519,9 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
let event: AddonCalendarEvent | AddonCalendarOfflineEventDBRecord;
try {
const result = await AddonCalendar.submitEvent(this.eventId, data);
const result = await AddonCalendar.submitEvent(this.eventId, data, {
reminders: this.reminders,
});
event = result.event;
CoreForms.triggerFormSubmittedEvent(this.formElement, result.sent, this.currentSite.getId());
@ -562,7 +566,10 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
if (event) {
CoreEvents.trigger(
AddonCalendarProvider.NEW_EVENT_EVENT,
{ eventId: event.id! },
{
eventId: event.id,
oldEventId: this.eventId,
},
this.currentSite.getId(),
);
} else {
@ -578,10 +585,15 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
* Discard an offline saved discussion.
*/
async discard(): Promise<void> {
if (!this.eventId) {
return;
}
try {
await CoreDomUtils.showConfirm(Translate.instant('core.areyousure'));
try {
await AddonCalendarOffline.deleteEvent(this.eventId!);
await AddonCalendarOffline.deleteEvent(this.eventId);
CoreForms.triggerFormCancelledEvent(this.formElement, this.currentSite.getId());
@ -620,6 +632,69 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
}
}
/**
* Init reminders.
*
* @return Promise resolved when done.
*/
protected async initReminders(): Promise<void> {
if (!this.notificationsEnabled) {
return;
}
// Check if default reminders are enabled.
const defaultTime = await AddonCalendar.getDefaultNotificationTime(this.currentSite.getId());
if (defaultTime === 0) {
return;
}
const data = AddonCalendarProvider.convertSecondsToValueAndUnit(defaultTime);
// Add default reminder.
this.reminders.push({
time: null,
value: data.value,
unit: data.unit,
label: AddonCalendar.getUnitValueLabel(data.value, data.unit, true),
});
}
/**
* Add a reminder.
*/
async addReminder(): Promise<void> {
const reminderTime = await CoreDomUtils.openModal<number>({
component: AddonCalendarReminderTimeModalComponent,
});
if (reminderTime === undefined) {
// User canceled.
return;
}
const data = AddonCalendarProvider.convertSecondsToValueAndUnit(reminderTime);
// Add reminder.
this.reminders.push({
time: reminderTime,
value: data.value,
unit: data.unit,
label: AddonCalendar.getUnitValueLabel(data.value, data.unit),
});
}
/**
* Remove a reminder.
*
* @param reminder The reminder to remove.
*/
removeReminder(reminder: AddonCalendarEventCandidateReminder): void {
const index = this.reminders.indexOf(reminder);
if (index != -1) {
this.reminders.splice(index, 1);
}
}
/**
* Page destroyed.
*/
@ -629,3 +704,5 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
}
}
type AddonCalendarEventCandidateReminder = Omit<AddonCalendarEventReminder, 'id'|'eventid'>;

View File

@ -21,8 +21,8 @@
[priority]="400" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event, true)"
[iconAction]="syncIcon" [closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item [hidden]="!canEdit || !event || !event.canedit || event.deleted" [priority]="300"
[content]="'core.edit' | translate" (action)="openEdit()" iconAction="fas-edit">
<core-context-menu-item [hidden]="!event || !event.canedit || event.deleted || (!canEdit && event.id > 0)"
[priority]="300" [content]="'core.edit' | translate" (action)="openEdit()" iconAction="fas-edit">
</core-context-menu-item>
<core-context-menu-item [hidden]="!event || !event.candelete || event.deleted" [priority]="200"
[content]="'core.delete' | translate" (action)="deleteEvent()"
@ -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.
@ -53,17 +52,16 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
protected eventId!: number;
protected siteHomeId: number;
protected newEventObserver: CoreEventObserver;
protected editEventObserver: CoreEventObserver;
protected syncObserver: CoreEventObserver;
protected manualSyncObserver: CoreEventObserver;
protected onlineObserver: Subscription;
protected defaultTimeChangedObserver: CoreEventObserver;
protected currentSiteId: string;
protected updateCurrentTime?: number;
eventLoaded = false;
notificationFormat?: string;
notificationMin?: string;
notificationMax?: string;
notificationTimeText?: string;
event?: AddonCalendarEventToDisplay;
courseId?: number;
courseName = '';
@ -72,19 +70,16 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
notificationsEnabled = false;
moduleUrl = '';
categoryPath = '';
currentTime?: number;
defaultTime = 0;
reminders: AddonCalendarReminderDBRecord[] = [];
currentTime = -1;
reminders: AddonCalendarEventReminder[] = [];
canEdit = false;
hasOffline = false;
isOnline = false;
syncIcon = CoreConstants.ICON_LOADING; // Sync icon.
monthNames?: string[];
constructor(
protected route: ActivatedRoute,
) {
this.notificationsEnabled = CoreLocalNotifications.isAvailable();
this.siteHomeId = CoreSites.getCurrentSiteHomeId();
this.currentSiteId = CoreSites.getCurrentSiteId();
@ -94,7 +89,16 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
// Listen for event edited. If current event is edited, reload the data.
this.editEventObserver = CoreEvents.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => {
if (data && data.eventId == this.eventId) {
if (data && data.eventId === this.eventId) {
this.eventLoaded = false;
this.refreshEvent(true, false);
}
}, this.currentSiteId);
// Listen for event created. If user edits the data of a new offline event or it's sent to server, this event is triggered.
this.newEventObserver = CoreEvents.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => {
if (this.eventId < 0 && data && (data.eventId === this.eventId || data.oldEventId === this.eventId)) {
this.eventId = data.eventId;
this.eventLoaded = false;
this.refreshEvent(true, false);
}
@ -121,21 +125,35 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
this.isOnline = CoreApp.isOnline();
});
});
// Reload reminders if default notification time changes.
this.defaultTimeChangedObserver = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
this.loadReminders();
if (this.event) {
AddonCalendar.scheduleEventsNotifications([this.event]);
}
}, this.currentSiteId);
// Set and update current time. Use a 5 seconds error margin.
this.currentTime = CoreTimeUtils.timestamp();
this.updateCurrentTime = window.setInterval(() => {
this.currentTime = CoreTimeUtils.timestamp();
}, 5000);
}
protected async initReminders(): Promise<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 +173,6 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
this.syncIcon = CoreConstants.ICON_LOADING;
this.fetchEvent();
this.initReminders();
}
/**
@ -166,48 +183,22 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
* @return Promise resolved when done.
*/
async fetchEvent(sync = false, showErrors = false): Promise<void> {
let deleted = false;
this.isOnline = CoreApp.isOnline();
if (sync) {
// Try to synchronize offline events.
try {
const result = await AddonCalendarSync.syncEvents();
if (result.warnings && result.warnings.length) {
CoreDomUtils.showErrorModal(result.warnings[0]);
}
const deleted = await this.syncEvents(showErrors);
if (result.deleted && result.deleted.indexOf(this.eventId) != -1) {
// This event was deleted during the sync.
deleted = true;
}
if (result.updated) {
// Trigger a manual sync event.
result.source = 'event';
CoreEvents.trigger(
AddonCalendarSyncProvider.MANUAL_SYNCED,
result,
this.currentSiteId,
);
}
} catch (error) {
if (showErrors) {
CoreDomUtils.showErrorModalDefault(error, 'core.errorsync', true);
}
if (deleted) {
return;
}
}
if (deleted) {
return;
}
try {
// Get the event data.
const event = await AddonCalendar.getEventById(this.eventId);
this.event = await AddonCalendarHelper.formatEventData(event);
if (this.eventId >= 0) {
const event = await AddonCalendar.getEventById(this.eventId);
this.event = await AddonCalendarHelper.formatEventData(event);
}
try {
const offlineEvent = AddonCalendarHelper.formatOfflineEventData(
@ -216,20 +207,27 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
// There is offline data, apply it.
this.hasOffline = true;
this.event = Object.assign(this.event, offlineEvent);
this.event = Object.assign(this.event || {}, offlineEvent);
} catch {
// No offline data.
this.hasOffline = false;
if (this.eventId < 0) {
// It's an offline event, but it wasn't found. Shouldn't happen.
CoreDomUtils.showErrorModal('Event not found.');
CoreNavigator.back();
return;
}
}
this.currentTime = CoreTimeUtils.timestamp();
this.notificationMin = CoreTimeUtils.userDate(this.currentTime * 1000, 'YYYY-MM-DDTHH:mm', false);
this.notificationMax = CoreTimeUtils.userDate(
(this.event!.timestart + this.event!.timeduration) * 1000,
'YYYY-MM-DDTHH:mm',
false,
);
if (!this.event) {
return; // At this point we should always have the event, adding this check to avoid TS errors.
}
// Load reminders, and re-schedule them if needed (maybe the event time has changed).
this.loadReminders();
AddonCalendar.scheduleEventsNotifications([this.event]);
// Reset some of the calculated data.
this.categoryPath = '';
@ -237,18 +235,19 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
this.courseUrl = '';
this.moduleUrl = '';
if (this.event!.moduleIcon) {
if (this.event.moduleIcon) {
// It's a module event, translate the module name to the current language.
const name = CoreCourse.translateModuleName(this.event!.modulename || '');
const name = CoreCourse.translateModuleName(this.event.modulename || '');
if (name.indexOf('core.mod_') === -1) {
this.event!.modulename = name;
this.event.modulename = name;
}
// Get the module URL.
this.moduleUrl = this.event!.url || '';
this.moduleUrl = this.event.url || '';
}
const promises: Promise<void>[] = [];
const event = this.event;
const courseId = this.event.courseid;
if (courseId != this.siteHomeId) {
@ -262,16 +261,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
// If it's a group event, get the name of the group.
if (courseId && this.event.groupid) {
promises.push(CoreGroups.getUserGroupsInCourse(courseId).then((groups) => {
const group = groups.find((group) => group.id == this.event!.groupid);
this.groupName = group ? group.name : '';
return;
}).catch(() => {
// Error getting groups, just don't show the group name.
this.groupName = '';
}));
promises.push(this.loadGroupName(this.event, courseId));
}
if (this.event.iscategoryevent && this.event.category) {
@ -286,14 +276,14 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
// Check if event was deleted in offine.
promises.push(AddonCalendarOffline.isEventDeleted(this.eventId).then((deleted) => {
this.event!.deleted = deleted;
event.deleted = deleted;
return;
}));
// Re-calculate the formatted time so it uses the device date.
promises.push(AddonCalendar.getCalendarTimeFormat().then(async (timeFormat) => {
this.event!.formattedtime = await AddonCalendar.formatEventTime(this.event!, timeFormat);
event.formattedtime = await AddonCalendar.formatEventTime(event, timeFormat);
return;
}));
@ -308,24 +298,88 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
}
/**
* Add a reminder for this event.
* Sync offline events.
*
* @param showErrors Whether to show sync errors to the user.
* @return Promise resolved with boolean: whether event was deleted on sync.
*/
async addNotificationTime(): Promise<void> {
if (this.notificationTimeText && this.event && this.event.id) {
let notificationTime = CoreTimeUtils.convertToTimestamp(this.notificationTimeText);
protected async syncEvents(showErrors = false): Promise<boolean> {
let deleted = false;
const currentTime = CoreTimeUtils.timestamp();
const minute = Math.floor(currentTime / 60) * 60;
// Check if the notification time is in the same minute as we are, so the notification is triggered.
if (notificationTime >= minute && notificationTime < minute + 60) {
notificationTime = currentTime + 1;
// Try to synchronize offline events.
try {
const result = await AddonCalendarSync.syncEvents();
if (result.warnings && result.warnings.length) {
CoreDomUtils.showErrorModal(result.warnings[0]);
}
await AddonCalendar.addEventReminder(this.event, notificationTime);
this.reminders = await AddonCalendar.getEventReminders(this.eventId);
this.notificationTimeText = undefined;
if (result.deleted && result.deleted.indexOf(this.eventId) != -1) {
// This event was deleted during the sync.
deleted = true;
} else if (this.eventId < 0 && result.offlineIdMap[this.eventId]) {
// Event was created, use the online ID.
this.eventId = result.offlineIdMap[this.eventId];
}
if (result.updated) {
// Trigger a manual sync event.
result.source = 'event';
CoreEvents.trigger(
AddonCalendarSyncProvider.MANUAL_SYNCED,
result,
this.currentSiteId,
);
}
} catch (error) {
if (showErrors) {
CoreDomUtils.showErrorModalDefault(error, 'core.errorsync', true);
}
}
return deleted;
}
/**
* Load group name.
*
* @param event Event.
* @param courseId Course ID.
* @return Promise resolved when done.
*/
protected async loadGroupName(event: AddonCalendarEventToDisplay, courseId: number): Promise<void> {
try {
const groups = await CoreGroups.getUserGroupsInCourse(courseId);
const group = groups.find((group) => group.id == event.groupid);
this.groupName = group ? group.name : '';
} catch {
// Error getting groups, just don't show the group name.
this.groupName = '';
}
}
/**
* Add a reminder for this event.
*/
async addReminder(): Promise<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 +399,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
try {
await AddonCalendar.deleteEventReminder(id);
this.reminders = await AddonCalendar.getEventReminders(this.eventId);
await this.loadReminders();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error deleting reminder');
} finally {
@ -387,7 +441,9 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
const promises: Promise<void>[] = [];
promises.push(AddonCalendar.invalidateEvent(this.eventId));
if (this.eventId > 0) {
promises.push(AddonCalendar.invalidateEvent(this.eventId));
}
promises.push(AddonCalendar.invalidateTimeFormat());
await CoreUtils.allPromisesIgnoringErrors(promises);
@ -454,9 +510,14 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
try {
const sent = await AddonCalendar.deleteEvent(this.event.id, this.event.name, deleteAll);
let onlineEventDeleted = false;
if (this.event.id < 0) {
await AddonCalendarOffline.deleteEvent(this.event.id);
} else {
onlineEventDeleted = await AddonCalendar.deleteEvent(this.event.id, this.event.name, deleteAll);
}
if (sent) {
if (onlineEventDeleted) {
// Event deleted, invalidate right days & months.
try {
await AddonCalendarHelper.refreshAfterChangeEvent(this.event, deleteAll ? this.event.eventcount : 1);
@ -466,12 +527,16 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
}
// Trigger an event.
CoreEvents.trigger(AddonCalendarProvider.DELETED_EVENT_EVENT, {
eventId: this.eventId,
sent: sent,
}, CoreSites.getCurrentSiteId());
if (this.event.id < 0) {
CoreEvents.trigger(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, {}, CoreSites.getCurrentSiteId());
} else {
CoreEvents.trigger(AddonCalendarProvider.DELETED_EVENT_EVENT, {
eventId: this.eventId,
sent: onlineEventDeleted,
}, CoreSites.getCurrentSiteId());
}
if (sent) {
if (onlineEventDeleted || this.event.id < 0) {
CoreDomUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000);
// Event deleted, close the view.
@ -532,11 +597,21 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
// Event was deleted, close the view.
CoreNavigator.back();
} else if (data.events && (!isManual || data.source != 'event')) {
const event = data.events.find((ev) => ev.id == this.eventId);
if (this.eventId < 0) {
if (data.offlineIdMap[this.eventId]) {
// Event was created, use the online ID.
this.eventId = data.offlineIdMap[this.eventId];
if (event) {
this.eventLoaded = false;
this.refreshEvent();
this.eventLoaded = false;
this.refreshEvent();
}
} else {
const event = data.events.find((ev) => ev.id == this.eventId);
if (event) {
this.eventLoaded = false;
this.refreshEvent();
}
}
}
}
@ -545,10 +620,12 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
* Page destroyed.
*/
ngOnDestroy(): void {
this.editEventObserver?.off();
this.syncObserver?.off();
this.manualSyncObserver?.off();
this.onlineObserver?.unsubscribe();
this.editEventObserver.off();
this.syncObserver.off();
this.manualSyncObserver.off();
this.onlineObserver.unsubscribe();
this.newEventObserver.off();
clearInterval(this.updateCurrentTime);
}
}

View File

@ -301,12 +301,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
* @param eventId Event to load.
*/
gotoEvent(eventId: number): void {
if (eventId < 0) {
// It's an offline event, go to the edit page.
this.openEdit(eventId);
} else {
CoreNavigator.navigateToSitePath(`/calendar/event/${eventId}`);
}
CoreNavigator.navigateToSitePath(`/calendar/event/${eventId}`);
}
/**

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.
@ -220,7 +222,7 @@ export class AddonCalendarHelperProvider {
formatOfflineEventData(event: AddonCalendarOfflineEventDBRecord): AddonCalendarEventToDisplay {
const eventFormatted: AddonCalendarEventToDisplay = {
id: event.id!,
id: event.id,
name: event.name,
timestart: event.timestart,
eventtype: event.eventtype,
@ -243,6 +245,8 @@ export class AddonCalendarHelperProvider {
format: 1,
visible: 1,
offline: true,
canedit: event.id < 0,
candelete: event.id < 0,
timeduration: 0,
};
@ -282,6 +286,55 @@ export class AddonCalendarHelperProvider {
}
}
/**
* Format reminders, adding calculated data.
*
* @param reminders Reminders.
* @param timestart Event timestart.
* @param siteId Site ID.
* @return Formatted reminders.
*/
async formatReminders(
reminders: AddonCalendarReminderDBRecord[],
timestart: number,
siteId?: string,
): Promise<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.
*
@ -335,7 +388,7 @@ export class AddonCalendarHelperProvider {
year: number,
month: number,
siteId?: string,
): Promise<{ daynames: Partial<AddonCalendarDayName>[]; weeks: Partial<AddonCalendarWeek>[] }> {
): Promise<{ daynames: Partial<AddonCalendarDayName>[]; weeks: AddonCalendarWeek[] }> {
const site = await CoreSites.getSite(siteId);
// Get starting week day user preference, fallback to site configuration.
let startWeekDayStr = site.getStoredConfig('calendar_startwday');
@ -344,7 +397,7 @@ export class AddonCalendarHelperProvider {
const today = moment();
const isCurrentMonth = today.year() == year && today.month() == month - 1;
const weeks: Partial<AddonCalendarWeek>[] = [];
const weeks: AddonCalendarWeek[] = [];
let date = moment({ year, month: month - 1, date: 1 });
for (let mday = 1; mday <= date.daysInMonth(); mday++) {
@ -371,7 +424,7 @@ export class AddonCalendarHelperProvider {
}
// Add day to current week.
weeks[weeks.length - 1].days!.push({
weeks[weeks.length - 1].days.push({
events: [],
hasevents: false,
mday: date.date(),
@ -450,11 +503,11 @@ export class AddonCalendarHelperProvider {
*/
getFilteredEvents(
events: AddonCalendarEventToDisplay[],
filter: AddonCalendarFilter,
filter: AddonCalendarFilter | undefined,
categories: { [id: number]: CoreCategoryData },
): AddonCalendarEventToDisplay[] {
// Do not filter.
if (!filter.filtered) {
if (!filter || !filter.filtered) {
return events;
}
@ -475,9 +528,9 @@ export class AddonCalendarHelperProvider {
* Check if an event should be displayed based on the filter.
*
* @param event Event object.
* @param categories Categories indexed by ID.
* @param courseId Course ID to filter.
* @param categoryId Category ID the course belongs to.
* @param categories Categories indexed by ID.
* @return Whether it should be displayed.
*/
protected shouldDisplayEvent(
@ -492,7 +545,7 @@ export class AddonCalendarHelperProvider {
}
if (event.eventtype == 'category' && categories) {
if (!event.categoryid || !Object.keys(categories).length) {
if (!event.categoryid || !Object.keys(categories).length || !categoryId) {
// We can't tell if the course belongs to the category, display them all.
return true;
}
@ -503,7 +556,7 @@ export class AddonCalendarHelperProvider {
}
// Check parent categories.
let category = categories[categoryId!];
let category = categories[categoryId];
while (category) {
if (!category.parent) {
// Category doesn't have parent, stop.
@ -567,7 +620,7 @@ export class AddonCalendarHelperProvider {
await AddonCalendar.getLocalEventsByRepeatIdFromLocalDb(eventData.repeatid, site.id);
await CoreUtils.allPromises(repeatedEvents.map((event) =>
AddonCalendar.invalidateEvent(event.id!)));
AddonCalendar.invalidateEvent(event.id)));
return;
}
@ -670,7 +723,7 @@ export class AddonCalendarHelperProvider {
*/
refreshAfterChangeEvent(
event: {
id?: number;
id: number;
repeatid?: number;
timestart: number;
},
@ -679,7 +732,7 @@ export class AddonCalendarHelperProvider {
): Promise<void> {
return this.refreshAfterChangeEvents(
[{
id: event.id!,
id: event.id,
repeatid: event.repeatid,
timestart: event.timestart,
repeated: repeated,
@ -725,3 +778,13 @@ export type AddonCalendarEventTypeOption = {
name: string;
value: AddonCalendarEventType;
};
/**
* Formatted event reminder.
*/
export type AddonCalendarEventReminder = AddonCalendarReminderDBRecord & {
value?: number; // Amount of time.
unit?: AddonCalendarReminderUnits; // Units.
timestamp?: number; // Timestamp (in seconds).
label?: string; // Label to represent the reminder.
};

View File

@ -110,7 +110,7 @@ export class AddonCalendarOfflineProvider {
async getAllEditedEventsIds(siteId?: string): Promise<number[]> {
const events = await this.getAllEditedEvents(siteId);
return events.map((event) => event.id!);
return events.map((event) => event.id);
}
/**
@ -215,20 +215,18 @@ export class AddonCalendarOfflineProvider {
/**
* Offline version for adding a new discussion to a forum.
*
* @param eventId Event ID. If it's a new event, set it to undefined/null.
* @param eventId Event ID. Negative value to edit offline event. If it's a new event, set it to undefined/null.
* @param data Event data.
* @param timeCreated The time the event was created. If not defined, current time.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the stored event.
*/
async saveEvent(
eventId: number | undefined,
data: AddonCalendarSubmitCreateUpdateFormDataWSParams,
timeCreated?: number,
siteId?: string,
): Promise<AddonCalendarOfflineEventDBRecord> {
const site = await CoreSites.getSite(siteId);
timeCreated = timeCreated || Date.now();
const timeCreated = Date.now();
const event: AddonCalendarOfflineEventDBRecord = {
id: eventId || -timeCreated,
name: data.name,

View File

@ -100,9 +100,10 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda
async syncEvents(siteId?: string): Promise<AddonCalendarSyncEvents> {
siteId = siteId || CoreSites.getCurrentSiteId();
if (this.isSyncing(AddonCalendarSyncProvider.SYNC_ID, siteId)) {
const currentSyncPromise = this.getOngoingSync(AddonCalendarSyncProvider.SYNC_ID, siteId);
if (currentSyncPromise) {
// There's already a sync ongoing for this site, return the promise.
return this.getOngoingSync(AddonCalendarSyncProvider.SYNC_ID, siteId)!;
return currentSyncPromise;
}
this.logger.debug('Try to sync calendar events for site ' + siteId);
@ -123,6 +124,7 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda
const result: AddonCalendarSyncEvents = {
warnings: [],
events: [],
offlineIdMap: {},
deleted: [],
toinvalidate: [],
updated: false,
@ -255,10 +257,13 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda
); // Clone the object because it will be modified in the submit function.
try {
const newEvent = await AddonCalendar.submitEventOnline(eventId > 0 ? eventId : 0, data, siteId);
const newEvent = await AddonCalendar.submitEventOnline(eventId, data, siteId);
result.updated = true;
result.events.push(newEvent);
if (eventId < 0) {
result.offlineIdMap[eventId] = newEvent.id;
}
// Add data to invalidate.
const numberOfRepetitions = data.repeat ? data.repeats :
@ -272,7 +277,7 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda
});
// Event sent, delete the offline data.
return AddonCalendarOffline.deleteEvent(event.id!, siteId);
return AddonCalendarOffline.deleteEvent(event.id, siteId);
} catch (error) {
if (!CoreUtils.isWebServiceError(error)) {
@ -283,7 +288,7 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda
// The WebService has thrown an error, this means that the event cannot be created. Delete it.
result.updated = true;
await AddonCalendarOffline.deleteEvent(event.id!, siteId);
await AddonCalendarOffline.deleteEvent(event.id, siteId);
// Event deleted, add a warning.
this.addOfflineDataDeletedWarning(result.warnings, event.name, error);
@ -297,6 +302,7 @@ export const AddonCalendarSync = makeSingleton(AddonCalendarSyncProvider);
export type AddonCalendarSyncEvents = {
warnings: string[];
events: AddonCalendarEvent[];
offlineIdMap: Record<number, number>; // Map offline ID with online ID for created events.
deleted: number[];
toinvalidate: AddonCalendarSyncInvalidateEvent[];
updated: boolean;

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);
@ -306,7 +366,7 @@ export class AddonCalendarProvider {
*/
async formatEventTime(
event: AddonCalendarEventToDisplay,
format: string,
format?: string,
useCommonWords = true,
seenDay?: number,
showTime = 0,
@ -548,7 +608,7 @@ export class AddonCalendarProvider {
* Get the configured default notification time.
*
* @param siteId ID of the site. If not defined, use current site.
* @return Promise resolved with the default time.
* @return Promise resolved with the default time (in seconds).
*/
async getDefaultNotificationTime(siteId?: string): Promise<number> {
siteId = siteId || CoreSites.getCurrentSiteId();
@ -656,18 +716,18 @@ export class AddonCalendarProvider {
eventConverted.iscategoryevent = originalEvent.eventtype == AddonCalendarEventType.CATEGORY;
eventConverted.normalisedeventtype = this.getEventType(recordAsRecord);
try {
eventConverted.category = CoreTextUtils.parseJSON(recordAsRecord.category!);
eventConverted.category = CoreTextUtils.parseJSON(recordAsRecord.category || '');
} catch {
// Ignore errors.
}
try {
eventConverted.course = CoreTextUtils.parseJSON(recordAsRecord.course!);
eventConverted.course = CoreTextUtils.parseJSON(recordAsRecord.course || '');
} catch {
// Ignore errors.
}
try {
eventConverted.subscription = CoreTextUtils.parseJSON(recordAsRecord.subscription!);
eventConverted.subscription = CoreTextUtils.parseJSON(recordAsRecord.subscription || '');
} catch {
// Ignore errors.
}
@ -678,24 +738,27 @@ export class AddonCalendarProvider {
/**
* Adds an event reminder and schedule a new notification.
*
* @param event Event to update its notification time.
* @param time New notification setting timestamp.
* @param event Event to set the reminder.
* @param time Amount of seconds of the reminder. Undefined for default reminder.
* @param siteId ID of the site the event belongs to. If not defined, use current site.
* @return Promise resolved when the notification is updated.
*/
async addEventReminder(
event: { id: number; timestart: number; timeduration: number; name: string},
time: number,
event: { id: number; timestart: number; name: string},
time?: number | null,
siteId?: string,
): Promise<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());
}
/**
@ -773,7 +836,7 @@ export class AddonCalendarProvider {
preSets.emergencyCache = false;
}
const response: AddonCalendarCalendarDay = await site.read('core_calendar_get_calendar_day_view', params, preSets);
this.storeEventsInLocalDB(response.events, siteId);
this.storeEventsInLocalDB(response.events, { siteId });
return response;
}
@ -855,6 +918,10 @@ export class AddonCalendarProvider {
const start = initialTime + (CoreConstants.SECONDS_DAY * daysToStart);
const end = start + (CoreConstants.SECONDS_DAY * daysInterval) - 1;
const events = {
courseids: <number[]> [],
groupids: <number[]> [],
};
const params: AddonCalendarGetCalendarEventsWSParams = {
options: {
userevents: true,
@ -862,23 +929,20 @@ export class AddonCalendarProvider {
timestart: start,
timeend: end,
},
events: {
courseids: [],
groupids: [],
},
events: events,
};
const promises: Promise<void>[] = [];
promises.push(CoreCourses.getUserCourses(false, siteId).then((courses) => {
params.events!.courseids = courses.map((course) => course.id);
params.events!.courseids.push(site.getSiteHomeId()); // Add front page.
events.courseids = courses.map((course) => course.id);
events.courseids.push(site.getSiteHomeId()); // Add front page.
return;
}));
promises.push(CoreGroups.getAllUserGroups(siteId).then((groups) => {
params.events!.groupids = groups.map((group) => group.id);
events.groupids = groups.map((group) => group.id);
return;
}));
@ -976,7 +1040,7 @@ export class AddonCalendarProvider {
const response = await site.read<AddonCalendarMonth>('core_calendar_get_calendar_monthly_view', params, preSets);
response.weeks.forEach((week) => {
week.days.forEach((day) => {
this.storeEventsInLocalDB(day.events as AddonCalendarCalendarEvent[], siteId);
this.storeEventsInLocalDB(day.events as AddonCalendarCalendarEvent[], { siteId });
});
});
@ -1022,6 +1086,35 @@ export class AddonCalendarProvider {
(categoryId ? categoryId : '');
}
/**
* Given a value and a unit, return the translated label.
*
* @param value Value.
* @param unit Unit.
* @param addDefaultLabel Whether to add the "Default" text.
* @return Translated label.
*/
getUnitValueLabel(value: number, unit: AddonCalendarReminderUnits, addDefaultLabel = false): string {
if (value === 0) {
return Translate.instant('core.settings.disabled');
}
const unitsLabel = value === 1 ?
REMINDER_UNITS_LABELS.single[unit] :
REMINDER_UNITS_LABELS.multi[unit];
const label = Translate.instant('addon.calendar.timebefore', {
units: Translate.instant(unitsLabel),
value: value,
});
if (addDefaultLabel) {
return Translate.instant('core.defaultvalue', { $a: label });
}
return label;
}
/**
* Get upcoming calendar events.
*
@ -1060,7 +1153,7 @@ export class AddonCalendarProvider {
}
const response = await site.read<AddonCalendarUpcoming>('core_calendar_get_calendar_upcoming_view', params, preSets);
this.storeEventsInLocalDB(response.events, siteId);
this.storeEventsInLocalDB(response.events, { siteId });
return response;
}
@ -1335,14 +1428,14 @@ export class AddonCalendarProvider {
*
* @param event Event to schedule.
* @param reminderId The reminder ID.
* @param time Notification setting time (in minutes). E.g. 10 means "notificate 10 minutes before start".
* @param time Notification timestamp (in seconds). Undefined for default time.
* @param siteId Site ID the event belongs to. If not defined, use current site.
* @return Promise resolved when the notification is scheduled.
*/
protected async scheduleEventNotification(
event: { id: number; timestart: number; name: string},
reminderId: number,
time: number,
time?: number | null,
siteId?: string,
): Promise<void> {
@ -1357,16 +1450,16 @@ export class AddonCalendarProvider {
return CoreLocalNotifications.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId);
}
if (time == -1) {
// If time is -1, get event default time to calculate the notification time.
if (!time) {
// Get event default time to calculate the notification time.
time = await this.getDefaultNotificationTime(siteId);
if (time == 0) {
if (time === 0) {
// Default notification time is disabled, do not show.
return CoreLocalNotifications.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId);
}
time = event.timestart - (time * 60);
time = event.timestart - time;
}
time = time * 1000;
@ -1417,17 +1510,18 @@ export class AddonCalendarProvider {
siteId = siteId || CoreSites.getCurrentSiteId();
const promises = events.map(async (event) => {
const timeEnd = (event.timestart + event.timeduration) * 1000;
if (timeEnd <= new Date().getTime()) {
// The event has finished already, don't schedule it.
if (event.timestart * 1000 <= Date.now()) {
// The event has already started, don't schedule it.
return this.deleteLocalEvent(event.id, siteId);
}
const reminders = await this.getEventReminders(event.id, siteId);
const p2 = reminders.map((reminder: AddonCalendarReminderDBRecord) =>
this.scheduleEventNotification(event, (reminder.id!), reminder.time, siteId));
const p2 = reminders.map((reminder) => {
const time = reminder.time ? event.timestart - reminder.time : reminder.time;
return this.scheduleEventNotification(event, reminder.id, time, siteId);
});
await Promise.all(p2);
});
@ -1454,20 +1548,29 @@ export class AddonCalendarProvider {
* Store an event in local DB as it is.
*
* @param event Event to store.
* @param siteId ID of the site the event belongs to. If not defined, use current site.
* @param options Options.
* @return Promise resolved when stored.
*/
async storeEventInLocalDb(event: AddonCalendarGetEventsEvent | AddonCalendarCalendarEvent, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
siteId = site.getId();
try {
await this.getEventFromLocalDb(event.id, site.id);
} catch {
// Event does not exist. Check if any reminder exists first.
const reminders = await this.getEventReminders(event.id, siteId);
protected async storeEventInLocalDb(
event: AddonCalendarGetEventsEvent | AddonCalendarCalendarEvent | AddonCalendarEvent,
options: AddonCalendarStoreEventsOptions = {},
): Promise<void> {
const site = await CoreSites.getSite(options.siteId);
const siteId = site.getId();
const addDefaultReminder = options.addDefaultReminder ?? true;
if (reminders.length == 0) {
this.addEventReminder(event, -1, siteId);
if (addDefaultReminder) {
// Add default reminder if the event isn't stored already and doesn't have any reminder.
try {
await this.getEventFromLocalDb(event.id, siteId);
} catch {
// Event does not exist.
const reminders = await this.getEventReminders(event.id, siteId);
if (reminders.length === 0) {
// No reminders, create the default one.
this.addEventReminder(event, undefined, siteId);
}
}
}
@ -1505,12 +1608,17 @@ export class AddonCalendarProvider {
viewurl: event.viewurl,
isactionevent: event.isactionevent ? 1 : 0,
url: event.url,
islastday: event.islastday ? 1 : 0,
popupname: event.popupname,
mindaytimestamp: event.mindaytimestamp,
maxdaytimestamp: event.maxdaytimestamp,
draggable: event.draggable ? 1 : 0,
});
if ('islastday' in event) {
eventRecord = Object.assign(eventRecord, {
islastday: event.islastday ? 1 : 0,
popupname: event.popupname,
mindaytimestamp: event.mindaytimestamp,
maxdaytimestamp: event.maxdaytimestamp,
draggable: event.draggable ? 1 : 0,
});
}
} else if ('uuid' in event) {
eventRecord = Object.assign(eventRecord, {
courseid: event.courseid,
@ -1527,25 +1635,20 @@ export class AddonCalendarProvider {
* Store events in local DB.
*
* @param events Events to store.
* @param siteId ID of the site the event belongs to. If not defined, use current site.
* @param options Options.
* @return Promise resolved when the events are stored.
*/
protected async storeEventsInLocalDB(
events: (AddonCalendarGetEventsEvent | AddonCalendarCalendarEvent)[],
siteId?: string,
events: (AddonCalendarGetEventsEvent | AddonCalendarCalendarEvent | AddonCalendarEvent)[],
options: AddonCalendarStoreEventsOptions = {},
): Promise<void> {
const site = await CoreSites.getSite(siteId);
siteId = site.getId();
await Promise.all(events.map((event: AddonCalendarGetEventsEvent| AddonCalendarCalendarEvent) =>
// If event does not exist on the DB, schedule the reminder.
this.storeEventInLocalDb(event, siteId)));
await Promise.all(events.map((event) => this.storeEventInLocalDb(event, options)));
}
/**
* Submit a calendar event.
*
* @param eventId ID of the event. If undefined/null, create a new event.
* @param eventId ID of the event. Negative value to edit offline event. If undefined/null, create a new event.
* @param formData Form data.
* @param timeCreated The time the event was created. Only if modifying a new offline event.
* @param forceOffline True to always save it in offline.
@ -1555,19 +1658,26 @@ export class AddonCalendarProvider {
async submitEvent(
eventId: number | undefined,
formData: AddonCalendarSubmitCreateUpdateFormDataWSParams,
timeCreated?: number,
forceOffline = false,
siteId?: string,
options: AddonCalendarSubmitEventOptions = {},
): Promise<{sent: boolean; event: AddonCalendarOfflineEventDBRecord | AddonCalendarEvent}> {
siteId = siteId || CoreSites.getCurrentSiteId();
const siteId = options.siteId || CoreSites.getCurrentSiteId();
// Function to store the event to be synchronized later.
const storeOffline = (): Promise<{ sent: boolean; event: AddonCalendarOfflineEventDBRecord }> =>
AddonCalendarOffline.saveEvent(eventId, formData, timeCreated, siteId).then((event) =>
({ sent: false, event }));
const storeOffline = async (): Promise<{ sent: boolean; event: AddonCalendarOfflineEventDBRecord }> => {
const event = await AddonCalendarOffline.saveEvent(eventId, formData, siteId);
if (forceOffline || !CoreApp.isOnline()) {
// Now save the reminders if any.
if (options.reminders) {
await CoreUtils.ignoreErrors(
Promise.all(options.reminders.map((reminder) => this.addEventReminder(event, reminder.time, siteId))),
);
}
return { sent: false, event };
};
if (options.forceOffline || !CoreApp.isOnline()) {
// App is offline, store the event.
return storeOffline();
}
@ -1579,6 +1689,13 @@ export class AddonCalendarProvider {
try {
const event = await this.submitEventOnline(eventId, formData, siteId);
// Now save the reminders if any.
if (options.reminders) {
await CoreUtils.ignoreErrors(
Promise.all(options.reminders.map((reminder) => this.addEventReminder(event, reminder.time, siteId))),
);
}
return ({ sent: true, event });
} catch (error) {
if (error && !CoreUtils.isWebServiceError(error)) {
@ -1594,7 +1711,7 @@ export class AddonCalendarProvider {
/**
* Submit an event, either to create it or to edit it. It will fail if offline or cannot connect.
*
* @param eventId ID of the event. If undefined/null, create a new event.
* @param eventId ID of the event. If undefined/null or negative number, create a new event.
* @param formData Form data.
* @param siteId Site ID. If not provided, current site.
* @return Promise resolved when done.
@ -1605,8 +1722,9 @@ export class AddonCalendarProvider {
siteId?: string,
): Promise<AddonCalendarEvent> {
const site = await CoreSites.getSite(siteId);
// Add data that is "hidden" in web.
formData.id = eventId;
formData.id = eventId > 0 ? eventId : 0;
formData.userid = site.getUserId();
formData.visible = 1;
formData.instance = 0;
@ -1615,12 +1733,14 @@ export class AddonCalendarProvider {
} else {
formData['_qf__core_calendar_local_event_forms_create'] = 1;
}
const params: AddonCalendarSubmitCreateUpdateFormWSParams = {
formdata: CoreUtils.objectToGetParams(formData),
};
const result =
await site.write<AddonCalendarSubmitCreateUpdateFormWSResponse>('core_calendar_submit_create_update_form', params);
if (result.validationerror) {
if (result.validationerror || !result.event) {
// Simulate a WS error.
throw new CoreWSError({
message: Translate.instant('core.invalidformdata'),
@ -1628,7 +1748,19 @@ export class AddonCalendarProvider {
});
}
return result.event!;
if (eventId < 0) {
// Offline event has been sent. Change reminders eventid if any.
await CoreUtils.ignoreErrors(
site.getDb().updateRecords(REMINDERS_TABLE, { eventid: result.event.id }, { eventid: eventId }),
);
}
if (formData.id === 0) {
// Store the new event in local DB.
await CoreUtils.ignoreErrors(this.storeEventInLocalDb(result.event, { addDefaultReminder: false, siteId }));
}
return result.event;
}
}
@ -2088,7 +2220,8 @@ type AddonCalendarSubmitCreateUpdateFormWSParams = {
/**
* Form data on AddonCalendarSubmitCreateUpdateFormWSParams.
*/
export type AddonCalendarSubmitCreateUpdateFormDataWSParams = Omit<AddonCalendarOfflineEventDBRecord, 'description'> & {
export type AddonCalendarSubmitCreateUpdateFormDataWSParams = Omit<AddonCalendarOfflineEventDBRecord, 'id'|'description'> & {
id?: number;
description?: {
text: string;
format: number;
@ -2143,6 +2276,7 @@ export type AddonCalendarEventToDisplay = Partial<AddonCalendarCalendarEvent> &
*/
export type AddonCalendarUpdatedEventEvent = {
eventId: number;
oldEventId?: number; // Old event ID. Used when an offline event is sent.
sent?: boolean;
};
@ -2154,3 +2288,30 @@ type AddonCalendarPushNotificationData = {
reminderId: number;
siteId: string;
};
/**
* Value and unit for reminders.
*/
export type AddonCalendarValueAndUnit = {
value: number;
unit: AddonCalendarReminderUnits;
};
/**
* Options to pass to submit event.
*/
export type AddonCalendarSubmitEventOptions = {
reminders?: {
time: number | null;
}[];
forceOffline?: boolean;
siteId?: string; // Site ID. If not defined, current site.
};
/**
* Options to pass to store events in local DB.
*/
export type AddonCalendarStoreEventsOptions = {
addDefaultReminder?: boolean; // Whether to add default reminder for new events with no reminders. Defaults to true.
siteId?: string; // Site ID. If not defined, current site.
};

View File

@ -135,7 +135,7 @@ export const CALENDAR_OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
};
export type AddonCalendarOfflineEventDBRecord = {
id?: number; // Negative for offline entries.
id: number; // Negative for offline entries.
name: string;
timestart: number;
eventtype: AddonCalendarEventType;

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,11 +214,51 @@ export const CALENDAR_SITE_SCHEMA: CoreSiteSchema = {
await db.migrateTable(oldTable, EVENTS_TABLE);
}
if (oldVersion < 4) {
// Migrate reminders. New format @since 4.0.
const defaultTime = await CoreUtils.ignoreErrors(AddonCalendar.getDefaultNotificationTime(siteId));
if (defaultTime) {
// Convert from minutes to seconds.
AddonCalendar.setDefaultNotificationTime(defaultTime * 60, siteId);
}
const records = await db.getAllRecords<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);
}));
}
},
};
export type AddonCalendarEventDBRecord = {
id?: number;
id: number;
name: string;
description: string;
eventtype: AddonCalendarEventType;
@ -257,7 +299,7 @@ export type AddonCalendarEventDBRecord = {
};
export type AddonCalendarReminderDBRecord = {
id?: number;
id: number;
eventid: number;
time: number;
time: number | null; // Number of seconds before the event, null for default time.
};

View File

@ -147,9 +147,10 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider<AddonMessage
const syncId = this.getSyncId(conversationId, userId);
if (this.isSyncing(syncId, siteId)) {
const currentSyncPromise = this.getOngoingSync(syncId, siteId);
if (currentSyncPromise) {
// There's already a sync ongoing for this conversation, return the promise.
return this.getOngoingSync(syncId, siteId)!;
return currentSyncPromise;
}
return this.addOngoingSync(syncId, this.performSyncDiscussion(conversationId, userId, siteId), siteId);

View File

@ -163,9 +163,10 @@ export class AddonModAssignSyncProvider extends CoreCourseActivitySyncBaseProvid
async syncAssign(assignId: number, siteId?: string): Promise<AddonModAssignSyncResult> {
siteId = siteId || CoreSites.getCurrentSiteId();
if (this.isSyncing(assignId, siteId)) {
const currentSyncPromise = this.getOngoingSync(assignId, siteId);
if (currentSyncPromise) {
// There's already a sync ongoing for this assign, return the promise.
return this.getOngoingSync(assignId, siteId)!;
return currentSyncPromise;
}
// Verify that assign isn't blocked.

View File

@ -122,9 +122,10 @@ export class AddonModChoiceSyncProvider extends CoreCourseActivitySyncBaseProvid
siteId = site.getId();
const syncId = this.getSyncId(choiceId, userId);
if (this.isSyncing(syncId, siteId)) {
const currentSyncPromise = this.getOngoingSync(syncId, siteId);
if (currentSyncPromise) {
// There's already a sync ongoing for this discussion, return the promise.
return this.getOngoingSync(syncId, siteId)!;
return currentSyncPromise;
}
this.logger.debug(`Try to sync choice '${choiceId}' for user '${userId}'`);

View File

@ -135,9 +135,10 @@ export class AddonModDataSyncProvider extends CoreCourseActivitySyncBaseProvider
syncDatabase(dataId: number, siteId?: string): Promise<AddonModDataSyncResult> {
siteId = siteId || CoreSites.getCurrentSiteId();
if (this.isSyncing(dataId, siteId)) {
const currentSyncPromise = this.getOngoingSync(dataId, siteId);
if (currentSyncPromise) {
// There's already a sync ongoing for this database, return the promise.
return this.getOngoingSync(dataId, siteId)!;
return currentSyncPromise;
}
// Verify that database isn't blocked.

View File

@ -130,9 +130,10 @@ export class AddonModFeedbackSyncProvider extends CoreCourseActivitySyncBaseProv
syncFeedback(feedbackId: number, siteId?: string): Promise<AddonModFeedbackSyncResult> {
siteId = siteId || CoreSites.getCurrentSiteId();
if (this.isSyncing(feedbackId, siteId)) {
const currentSyncPromise = this.getOngoingSync(feedbackId, siteId);
if (currentSyncPromise) {
// There's already a sync ongoing for this feedback, return the promise.
return this.getOngoingSync(feedbackId, siteId)!;
return currentSyncPromise;
}
// Verify that feedback isn't blocked.

View File

@ -198,10 +198,11 @@ export class AddonModForumSyncProvider extends CoreCourseActivitySyncBaseProvide
siteId = siteId || CoreSites.getCurrentSiteId();
const syncId = this.getForumSyncId(forumId, userId);
const currentSyncPromise = this.getOngoingSync(syncId, siteId);
if (this.isSyncing(syncId, siteId)) {
if (currentSyncPromise) {
// There's already a sync ongoing for this discussion, return the promise.
return this.getOngoingSync(syncId, siteId)!;
return currentSyncPromise;
}
// Verify that forum isn't blocked.
@ -429,10 +430,11 @@ export class AddonModForumSyncProvider extends CoreCourseActivitySyncBaseProvide
siteId = siteId || CoreSites.getCurrentSiteId();
const syncId = this.getDiscussionSyncId(discussionId, userId);
const currentSyncPromise = this.getOngoingSync(syncId, siteId);
if (this.isSyncing(syncId, siteId)) {
if (currentSyncPromise) {
// There's already a sync ongoing for this discussion, return the promise.
return this.getOngoingSync(syncId, siteId)!;
return currentSyncPromise;
}
// Verify that forum isn't blocked.

View File

@ -144,9 +144,10 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv
siteId = siteId || CoreSites.getCurrentSiteId();
const syncId = this.getGlossarySyncId(glossaryId, userId);
if (this.isSyncing(syncId, siteId)) {
const currentSyncPromise = this.getOngoingSync(syncId, siteId);
if (currentSyncPromise) {
// There's already a sync ongoing for this glossary, return the promise.
return this.getOngoingSync(syncId, siteId)!;
return currentSyncPromise;
}
// Verify that glossary isn't blocked.

View File

@ -108,9 +108,10 @@ export class AddonModH5PActivitySyncProvider extends CoreCourseActivitySyncBaseP
throw new CoreNetworkError();
}
if (this.isSyncing(contextId, siteId)) {
const currentSyncPromise = this.getOngoingSync(contextId, siteId);
if (currentSyncPromise) {
// There's already a sync ongoing for this discussion, return the promise.
return this.getOngoingSync(contextId, siteId)!;
return currentSyncPromise;
}
return this.addOngoingSync(contextId, this.syncActivityData(contextId, siteId), siteId);

View File

@ -263,9 +263,10 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
syncQuiz(quiz: AddonModQuizQuizWSData, askPreflight?: boolean, siteId?: string): Promise<AddonModQuizSyncResult> {
siteId = siteId || CoreSites.getCurrentSiteId();
if (this.isSyncing(quiz.id, siteId)) {
const currentSyncPromise = this.getOngoingSync(quiz.id, siteId);
if (currentSyncPromise) {
// There's already a sync ongoing for this quiz, return the promise.
return this.getOngoingSync(quiz.id, siteId)!;
return currentSyncPromise;
}
// Verify that quiz isn't blocked.

View File

@ -579,9 +579,10 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide
syncScorm(scorm: AddonModScormScorm, siteId?: string): Promise<AddonModScormSyncResult> {
siteId = siteId || CoreSites.getCurrentSiteId();
if (this.isSyncing(scorm.id, siteId)) {
const currentSyncPromise = this.getOngoingSync(scorm.id, siteId);
if (currentSyncPromise) {
// There's already a sync ongoing for this SCORM, return the promise.
return this.getOngoingSync(scorm.id, siteId)!;
return currentSyncPromise;
}
// Verify that SCORM isn't blocked.

View File

@ -124,10 +124,11 @@ export class AddonModSurveySyncProvider extends CoreCourseActivitySyncBaseProvid
userId = userId || site.getUserId();
const syncId = this.getSyncId(surveyId, userId);
const currentSyncPromise = this.getOngoingSync(syncId, siteId);
if (this.isSyncing(syncId, siteId)) {
if (currentSyncPromise) {
// There's already a sync ongoing for this site, return the promise.
return this.getOngoingSync(syncId, siteId)!;
return currentSyncPromise;
}
this.logger.debug(`Try to sync survey '${surveyId}' for user '${userId}'`);

View File

@ -167,10 +167,11 @@ export class AddonModWikiSyncProvider extends CoreSyncBaseProvider<AddonModWikiS
siteId = siteId || CoreSites.getCurrentSiteId();
const subwikiBlockId = this.getSubwikiBlockId(subwikiId, wikiId, userId, groupId);
const currentSyncPromise = this.getOngoingSync(subwikiBlockId, siteId);
if (this.isSyncing(subwikiBlockId, siteId)) {
if (currentSyncPromise) {
// There's already a sync ongoing for this subwiki, return the promise.
return this.getOngoingSync(subwikiBlockId, siteId)!;
return currentSyncPromise;
}
// Verify that subwiki isn't blocked.

View File

@ -129,9 +129,10 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider<AddonModW
syncWorkshop(workshopId: number, siteId?: string): Promise<AddonModWorkshopSyncResult> {
siteId = siteId || CoreSites.getCurrentSiteId();
if (this.isSyncing(workshopId, siteId)) {
const currentSyncPromise = this.getOngoingSync(workshopId, siteId);
if (currentSyncPromise) {
// There's already a sync ongoing for this discussion, return the promise.
return this.getOngoingSync(workshopId, siteId)!;
return currentSyncPromise;
}
// Verify that workshop isn't blocked.

View File

@ -112,9 +112,10 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider<AddonNotesSyncR
syncNotes(courseId: number, siteId?: string): Promise<AddonNotesSyncResult> {
siteId = siteId || CoreSites.getCurrentSiteId();
if (this.isSyncing(courseId, siteId)) {
const currentSyncPromise = this.getOngoingSync(courseId, siteId);
if (currentSyncPromise) {
// There's already a sync ongoing for notes, return the promise.
return this.getOngoingSync(courseId, siteId)!;
return currentSyncPromise;
}
this.logger.debug('Try to sync notes for course ' + courseId);

View File

@ -105,7 +105,7 @@ export class CoreSyncBaseProvider<T = void> {
try {
return await promise;
} finally {
delete this.syncPromises[siteId!][uniqueId];
delete this.syncPromises[siteId][uniqueId];
}
}
@ -133,15 +133,11 @@ export class CoreSyncBaseProvider<T = void> {
*/
getOngoingSync(id: string | number, siteId?: string): Promise<T> | undefined {
siteId = siteId || CoreSites.getCurrentSiteId();
if (!this.isSyncing(id, siteId)) {
return;
}
// There's already a sync ongoing for this id, return the promise.
const uniqueId = this.getUniqueSyncId(id);
return this.syncPromises[siteId][uniqueId];
if (this.syncPromises[siteId] && this.syncPromises[siteId][uniqueId] !== undefined) {
return this.syncPromises[siteId][uniqueId];
}
}
/**
@ -223,11 +219,7 @@ export class CoreSyncBaseProvider<T = void> {
* @return Whether it's synchronizing.
*/
isSyncing(id: string | number, siteId?: string): boolean {
siteId = siteId || CoreSites.getCurrentSiteId();
const uniqueId = this.getUniqueSyncId(id);
return !!(this.syncPromises[siteId] && this.syncPromises[siteId][uniqueId]);
return !!this.getOngoingSync(id, siteId);
}
/**
@ -331,10 +323,10 @@ export class CoreSyncBaseProvider<T = void> {
*/
protected get componentTranslate(): string {
if (!this.componentTranslateInternal) {
this.componentTranslateInternal = Translate.instant(this.componentTranslatableString);
this.componentTranslateInternal = <string> Translate.instant(this.componentTranslatableString);
}
return this.componentTranslateInternal!;
return this.componentTranslateInternal;
}
}

View File

@ -591,8 +591,7 @@ export class CoreSite {
return response;
} catch (error) {
if (error.errorcode == 'invalidtoken' ||
(error.errorcode == 'accessexception' && error.message.indexOf('Invalid token - token expired') > -1)) {
if (CoreUtils.isExpiredTokenError(error)) {
if (initialToken !== this.token && !retrying) {
// Token has changed, retry with the new token.
preSets.getFromCache = false; // Don't check cache now. Also, it will skip ongoingRequests.

View File

@ -158,10 +158,11 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider<CoreCommentsS
siteId = siteId || CoreSites.getCurrentSiteId();
const syncId = this.getSyncId(contextLevel, instanceId, component, itemId, area);
const currentSyncPromise = this.getOngoingSync(syncId, siteId);
if (this.isSyncing(syncId, siteId)) {
if (currentSyncPromise) {
// There's already a sync ongoing for comments, return the promise.
return this.getOngoingSync(syncId, siteId)!;
return currentSyncPromise;
}
this.logger.debug('Try to sync comments ' + syncId + ' in site ' + siteId);

View File

@ -117,9 +117,10 @@ export class CoreCourseSyncProvider extends CoreSyncBaseProvider<CoreCourseSyncR
async syncCourse(courseId: number, siteId?: string): Promise<CoreCourseSyncResult> {
siteId = siteId || CoreSites.getCurrentSiteId();
if (this.isSyncing(courseId, siteId)) {
const currentSyncPromise = this.getOngoingSync(courseId, siteId);
if (currentSyncPromise) {
// There's already a sync ongoing for this discussion, return the promise.
return this.getOngoingSync(courseId, siteId)!;
return currentSyncPromise;
}
this.logger.debug(`Try to sync course '${courseId}'`);

View File

@ -524,7 +524,13 @@ export class CorePushNotificationsProvider {
try {
response = await site.write<CoreUserRemoveUserDeviceWSResponse>('core_user_remove_user_device', data);
} catch (error) {
if (CoreUtils.isWebServiceError(error)) {
if (CoreUtils.isWebServiceError(error) || CoreUtils.isExpiredTokenError(error)) {
// Cannot unregister. Don't try again.
await CoreUtils.ignoreErrors(db.deleteRecords(PENDING_UNREGISTER_TABLE_NAME, {
token: site.getToken(),
siteid: site.getId(),
}));
throw error;
}

View File

@ -157,9 +157,10 @@ export class CoreRatingSyncProvider extends CoreSyncBaseProvider<CoreRatingSyncI
siteId = siteId || CoreSites.getCurrentSiteId();
const syncId = this.getItemSetSyncId(component, ratingArea, contextLevel, instanceId, itemSetId);
if (this.isSyncing(syncId, siteId)) {
const currentSyncPromise = this.getOngoingSync(syncId, siteId);
if (currentSyncPromise) {
// There's already a sync ongoing for this item set, return the promise.
return this.getOngoingSync(syncId, siteId)!;
return currentSyncPromise;
}
this.logger.debug(`Try to sync ratings of component '${component}' rating area '${ratingArea}'` +

View File

@ -54,10 +54,11 @@ export class CoreUserSyncProvider extends CoreSyncBaseProvider<string[]> {
siteId = siteId || CoreSites.getCurrentSiteId();
const syncId = 'preferences';
const currentSyncPromise = this.getOngoingSync(syncId, siteId);
if (this.isSyncing(syncId, siteId)) {
if (currentSyncPromise) {
// There's already a sync ongoing, return the promise.
return this.getOngoingSync(syncId, siteId)!;
return currentSyncPromise;
}
this.logger.debug('Try to sync user preferences');

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.",

View File

@ -653,9 +653,19 @@ export class CoreLocalNotificationsProvider {
*/
async trigger(notification: ILocalNotification): Promise<number> {
const db = await this.appDB;
let time = Date.now();
if (notification.trigger?.at) {
// The type says "at" is a Date, but in Android we can receive timestamps instead.
if (typeof notification.trigger.at === 'number') {
time = <number> notification.trigger.at;
} else {
time = notification.trigger.at.getTime();
}
}
const entry = {
id: notification.id,
at: notification.trigger && notification.trigger.at ? notification.trigger.at.getTime() : Date.now(),
at: time,
};
return db.insertRecord(TRIGGERED_TABLE_NAME, entry);

View File

@ -850,7 +850,7 @@ export class CoreUtilsProvider {
}
/**
* Given an error returned by a WS call, check if the error is generated by the app or it has been returned by the WebSwervice.
* Given an error returned by a WS call, check if the error is generated by the app or it has been returned by the WebService.
*
* @param error Error to check.
* @return Whether the error was returned by the WebService.
@ -858,10 +858,22 @@ export class CoreUtilsProvider {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isWebServiceError(error: any): boolean {
return error && (typeof error.warningcode != 'undefined' || (typeof error.errorcode != 'undefined' &&
error.errorcode != 'invalidtoken' && error.errorcode != 'userdeleted' && error.errorcode != 'upgraderunning' &&
error.errorcode != 'userdeleted' && error.errorcode != 'upgraderunning' &&
error.errorcode != 'forcepasswordchangenotice' && error.errorcode != 'usernotfullysetup' &&
error.errorcode != 'sitepolicynotagreed' && error.errorcode != 'sitemaintenance' &&
(error.errorcode != 'accessexception' || error.message.indexOf('Invalid token - token expired') == -1)));
!this.isExpiredTokenError(error)));
}
/**
* Given an error returned by a WS call, check if the error is a token expired error.
*
* @param error Error to check.
* @return Whether the error is a token expired error.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isExpiredTokenError(error: any): boolean {
return error.errorcode === 'invalidtoken' ||
(error.errorcode === 'accessexception' && error.message.includes('Invalid token - token expired'));
}
/**