MOBILE-3909 calendar: Allow setting reminders when creating event

main
Dani Palou 2021-11-15 14:29:51 +01:00
parent b31b0764a5
commit e86d49742a
5 changed files with 201 additions and 44 deletions

View File

@ -225,6 +225,32 @@
</ion-item> </ion-item>
</ion-radio-group> </ion-radio-group>
</div> </div>
<!-- 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>
</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>
<ion-item>
<ion-label>
<ion-button expand="block" color="light" (click)="addReminder()">
{{ 'addon.calendar.setnewreminder' | translate }}
</ion-button>
</ion-label>
</ion-item>
</ng-container>
</div> </div>
<ion-item> <ion-item>

View File

@ -33,7 +33,7 @@ import {
AddonCalendarSubmitCreateUpdateFormDataWSParams, AddonCalendarSubmitCreateUpdateFormDataWSParams,
} from '../../services/calendar'; } from '../../services/calendar';
import { AddonCalendarOffline } from '../../services/calendar-offline'; import { AddonCalendarOffline } from '../../services/calendar-offline';
import { AddonCalendarEventTypeOption, AddonCalendarHelper } from '../../services/calendar-helper'; import { AddonCalendarEventReminder, AddonCalendarEventTypeOption, AddonCalendarHelper } from '../../services/calendar-helper';
import { AddonCalendarSync, AddonCalendarSyncProvider } from '../../services/calendar-sync'; import { AddonCalendarSync, AddonCalendarSyncProvider } from '../../services/calendar-sync';
import { CoreSite } from '@classes/site'; import { CoreSite } from '@classes/site';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
@ -43,6 +43,8 @@ import { CoreError } from '@classes/errors/error';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CanLeave } from '@guards/can-leave'; import { CanLeave } from '@guards/can-leave';
import { CoreForms } from '@singletons/form'; import { CoreForms } from '@singletons/form';
import { CoreLocalNotifications } from '@services/local-notifications';
import { AddonCalendarReminderTimeModalComponent } from '@addons/calendar/components/reminder-time-modal/reminder-time-modal';
/** /**
* Page that displays a form to create/edit an event. * Page that displays a form to create/edit an event.
@ -83,6 +85,10 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
groupControl: FormControl; groupControl: FormControl;
descriptionControl: FormControl; descriptionControl: FormControl;
// Reminders.
notificationsEnabled = false;
reminders: AddonCalendarEventCandidateReminder[] = [];
protected courseId!: number; protected courseId!: number;
protected originalData?: AddonCalendarOfflineEventDBRecord; protected originalData?: AddonCalendarOfflineEventDBRecord;
protected currentSite: CoreSite; protected currentSite: CoreSite;
@ -95,6 +101,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
protected fb: FormBuilder, protected fb: FormBuilder,
) { ) {
this.currentSite = CoreSites.getRequiredCurrentSite(); this.currentSite = CoreSites.getRequiredCurrentSite();
this.notificationsEnabled = CoreLocalNotifications.isAvailable();
this.errors = { this.errors = {
required: Translate.instant('core.required'), required: Translate.instant('core.required'),
}; };
@ -140,6 +147,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
this.form.addControl('timedurationuntil', this.fb.control(currentDate)); this.form.addControl('timedurationuntil', this.fb.control(currentDate));
this.form.addControl('courseid', this.fb.control(this.courseId)); this.form.addControl('courseid', this.fb.control(this.courseId));
this.initReminders();
this.fetchData().finally(() => { this.fetchData().finally(() => {
this.originalData = CoreUtils.clone(this.form.value); this.originalData = CoreUtils.clone(this.form.value);
this.loaded = true; this.loaded = true;
@ -519,7 +527,9 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
let event: AddonCalendarEvent | AddonCalendarOfflineEventDBRecord; let event: AddonCalendarEvent | AddonCalendarOfflineEventDBRecord;
try { try {
const result = await AddonCalendar.submitEvent(this.eventId, data); const result = await AddonCalendar.submitEvent(this.eventId, data, {
reminders: this.reminders,
});
event = result.event; event = result.event;
CoreForms.triggerFormSubmittedEvent(this.formElement, result.sent, this.currentSite.getId()); CoreForms.triggerFormSubmittedEvent(this.formElement, result.sent, this.currentSite.getId());
@ -630,6 +640,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. * Page destroyed.
*/ */
@ -639,3 +712,5 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
} }
} }
type AddonCalendarEventCandidateReminder = Omit<AddonCalendarEventReminder, 'id'|'eventid'>;

View File

@ -215,20 +215,18 @@ export class AddonCalendarOfflineProvider {
/** /**
* Offline version for adding a new discussion to a forum. * 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 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. * @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the stored event. * @return Promise resolved with the stored event.
*/ */
async saveEvent( async saveEvent(
eventId: number | undefined, eventId: number | undefined,
data: AddonCalendarSubmitCreateUpdateFormDataWSParams, data: AddonCalendarSubmitCreateUpdateFormDataWSParams,
timeCreated?: number,
siteId?: string, siteId?: string,
): Promise<AddonCalendarOfflineEventDBRecord> { ): Promise<AddonCalendarOfflineEventDBRecord> {
const site = await CoreSites.getSite(siteId); const site = await CoreSites.getSite(siteId);
timeCreated = timeCreated || Date.now(); const timeCreated = Date.now();
const event: AddonCalendarOfflineEventDBRecord = { const event: AddonCalendarOfflineEventDBRecord = {
id: eventId || -timeCreated, id: eventId || -timeCreated,
name: data.name, name: data.name,

View File

@ -744,7 +744,7 @@ export class AddonCalendarProvider {
* @return Promise resolved when the notification is updated. * @return Promise resolved when the notification is updated.
*/ */
async addEventReminder( async addEventReminder(
event: { id: number; timestart: number; timeduration: number; name: string}, event: { id: number; timestart: number; name: string},
time?: number | null, time?: number | null,
siteId?: string, siteId?: string,
): Promise<void> { ): Promise<void> {
@ -836,7 +836,7 @@ export class AddonCalendarProvider {
preSets.emergencyCache = false; preSets.emergencyCache = false;
} }
const response: AddonCalendarCalendarDay = await site.read('core_calendar_get_calendar_day_view', params, preSets); const response: AddonCalendarCalendarDay = await site.read('core_calendar_get_calendar_day_view', params, preSets);
this.storeEventsInLocalDB(response.events, siteId); this.storeEventsInLocalDB(response.events, { siteId });
return response; return response;
} }
@ -1040,7 +1040,7 @@ export class AddonCalendarProvider {
const response = await site.read<AddonCalendarMonth>('core_calendar_get_calendar_monthly_view', params, preSets); const response = await site.read<AddonCalendarMonth>('core_calendar_get_calendar_monthly_view', params, preSets);
response.weeks.forEach((week) => { response.weeks.forEach((week) => {
week.days.forEach((day) => { week.days.forEach((day) => {
this.storeEventsInLocalDB(day.events as AddonCalendarCalendarEvent[], siteId); this.storeEventsInLocalDB(day.events as AddonCalendarCalendarEvent[], { siteId });
}); });
}); });
@ -1153,7 +1153,7 @@ export class AddonCalendarProvider {
} }
const response = await site.read<AddonCalendarUpcoming>('core_calendar_get_calendar_upcoming_view', params, preSets); const response = await site.read<AddonCalendarUpcoming>('core_calendar_get_calendar_upcoming_view', params, preSets);
this.storeEventsInLocalDB(response.events, siteId); this.storeEventsInLocalDB(response.events, { siteId });
return response; return response;
} }
@ -1548,21 +1548,29 @@ export class AddonCalendarProvider {
* Store an event in local DB as it is. * Store an event in local DB as it is.
* *
* @param event Event to store. * @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. * @return Promise resolved when stored.
*/ */
async storeEventInLocalDb(event: AddonCalendarGetEventsEvent | AddonCalendarCalendarEvent, siteId?: string): Promise<void> { protected async storeEventInLocalDb(
const site = await CoreSites.getSite(siteId); event: AddonCalendarGetEventsEvent | AddonCalendarCalendarEvent | AddonCalendarEvent,
siteId = site.getId(); options: AddonCalendarStoreEventsOptions = {},
try { ): Promise<void> {
await this.getEventFromLocalDb(event.id, site.id); const site = await CoreSites.getSite(options.siteId);
} catch { const siteId = site.getId();
// Event does not exist. Check if any reminder exists first. const addDefaultReminder = options.addDefaultReminder ?? true;
const reminders = await this.getEventReminders(event.id, siteId);
if (reminders.length == 0) { if (addDefaultReminder) {
// No reminders, create the default one. // Add default reminder if the event isn't stored already and doesn't have any reminder.
this.addEventReminder(event, undefined, siteId); 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);
}
} }
} }
@ -1600,12 +1608,17 @@ export class AddonCalendarProvider {
viewurl: event.viewurl, viewurl: event.viewurl,
isactionevent: event.isactionevent ? 1 : 0, isactionevent: event.isactionevent ? 1 : 0,
url: event.url, 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) { } else if ('uuid' in event) {
eventRecord = Object.assign(eventRecord, { eventRecord = Object.assign(eventRecord, {
courseid: event.courseid, courseid: event.courseid,
@ -1622,23 +1635,20 @@ export class AddonCalendarProvider {
* Store events in local DB. * Store events in local DB.
* *
* @param events Events to store. * @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. * @return Promise resolved when the events are stored.
*/ */
protected async storeEventsInLocalDB( protected async storeEventsInLocalDB(
events: (AddonCalendarGetEventsEvent | AddonCalendarCalendarEvent)[], events: (AddonCalendarGetEventsEvent | AddonCalendarCalendarEvent | AddonCalendarEvent)[],
siteId?: string, options: AddonCalendarStoreEventsOptions = {},
): Promise<void> { ): Promise<void> {
const site = await CoreSites.getSite(siteId); await Promise.all(events.map((event) => this.storeEventInLocalDb(event, options)));
siteId = site.getId();
await Promise.all(events.map((event) => this.storeEventInLocalDb(event, siteId)));
} }
/** /**
* Submit a calendar event. * 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 formData Form data.
* @param timeCreated The time the event was created. Only if modifying a new offline event. * @param timeCreated The time the event was created. Only if modifying a new offline event.
* @param forceOffline True to always save it in offline. * @param forceOffline True to always save it in offline.
@ -1648,19 +1658,26 @@ export class AddonCalendarProvider {
async submitEvent( async submitEvent(
eventId: number | undefined, eventId: number | undefined,
formData: AddonCalendarSubmitCreateUpdateFormDataWSParams, formData: AddonCalendarSubmitCreateUpdateFormDataWSParams,
timeCreated?: number, options: AddonCalendarSubmitEventOptions = {},
forceOffline = false,
siteId?: string,
): Promise<{sent: boolean; event: AddonCalendarOfflineEventDBRecord | AddonCalendarEvent}> { ): 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. // Function to store the event to be synchronized later.
const storeOffline = (): Promise<{ sent: boolean; event: AddonCalendarOfflineEventDBRecord }> => const storeOffline = async (): Promise<{ sent: boolean; event: AddonCalendarOfflineEventDBRecord }> => {
AddonCalendarOffline.saveEvent(eventId, formData, timeCreated, siteId).then((event) => const event = await AddonCalendarOffline.saveEvent(eventId, formData, siteId);
({ sent: false, event }));
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. // App is offline, store the event.
return storeOffline(); return storeOffline();
} }
@ -1672,6 +1689,13 @@ export class AddonCalendarProvider {
try { try {
const event = await this.submitEventOnline(eventId, formData, siteId); 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 }); return ({ sent: true, event });
} catch (error) { } catch (error) {
if (error && !CoreUtils.isWebServiceError(error)) { if (error && !CoreUtils.isWebServiceError(error)) {
@ -1731,6 +1755,11 @@ export class AddonCalendarProvider {
); );
} }
if (formData.id === 0) {
// Store the new event in local DB.
await CoreUtils.ignoreErrors(this.storeEventInLocalDb(result.event, { addDefaultReminder: false, siteId }));
}
return result.event; return result.event;
} }
@ -2267,3 +2296,22 @@ export type AddonCalendarValueAndUnit = {
value: number; value: number;
unit: AddonCalendarReminderUnits; unit: AddonCalendarReminderUnits;
}; };
/**
* Options to pass to submit event.
*/
export type AddonCalendarSubmitEventOptions = {
reminders?: {
time: number | null;
}[];
forceOffline?: boolean;
siteId?: string; // Site ID. If not defined, current site.
};
/**
* Options to pass to store events in local DB.
*/
export type AddonCalendarStoreEventsOptions = {
addDefaultReminder?: boolean; // Whether to add default reminder for new events with no reminders. Defaults to true.
siteId?: string; // Site ID. If not defined, current site.
};

View File

@ -653,9 +653,19 @@ export class CoreLocalNotificationsProvider {
*/ */
async trigger(notification: ILocalNotification): Promise<number> { async trigger(notification: ILocalNotification): Promise<number> {
const db = await this.appDB; 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 = { const entry = {
id: notification.id, id: notification.id,
at: notification.trigger && notification.trigger.at ? notification.trigger.at.getTime() : Date.now(), at: time,
}; };
return db.insertRecord(TRIGGERED_TABLE_NAME, entry); return db.insertRecord(TRIGGERED_TABLE_NAME, entry);