commit
6703b89b94
|
@ -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",
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'>;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}'`);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}'`);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}'`);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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}'` +
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue