MOBILE-3936 reminders: Add reminders to courses and activities

main
Pau Ferrer Ocaña 2022-11-08 17:31:02 +01:00
parent 109d4bd2c5
commit 5d910ea5b4
14 changed files with 278 additions and 80 deletions

View File

@ -29,6 +29,7 @@ import { CoreCourseModuleManualCompletionComponent } from './module-manual-compl
import { CoreCourseModuleNavigationComponent } from './module-navigation/module-navigation'; import { CoreCourseModuleNavigationComponent } from './module-navigation/module-navigation';
import { CoreCourseModuleSummaryComponent } from './module-summary/module-summary'; import { CoreCourseModuleSummaryComponent } from './module-summary/module-summary';
import { CoreCourseCourseIndexTourComponent } from './course-index-tour/course-index-tour'; import { CoreCourseCourseIndexTourComponent } from './course-index-tour/course-index-tour';
import { CoreRemindersComponentsModule } from '@features/reminders/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -48,6 +49,7 @@ import { CoreCourseCourseIndexTourComponent } from './course-index-tour/course-i
], ],
imports: [ imports: [
CoreBlockComponentsModule, CoreBlockComponentsModule,
CoreRemindersComponentsModule,
CoreSharedModule, CoreSharedModule,
], ],
exports: [ exports: [

View File

@ -26,10 +26,9 @@
<!-- Activity dates. --> <!-- Activity dates. -->
<div *ngIf="module.dates && module.dates.length" class="core-module-dates core-module-info-box-section"> <div *ngIf="module.dates && module.dates.length" class="core-module-dates core-module-info-box-section">
<p *ngFor="let date of module.dates"> <core-reminders-date *ngFor="let date of module.dates" [component]="component" [instanceId]="module.id" [type]="date.dataid"
<ion-icon name="fas-calendar" aria-hidden="true"></ion-icon><strong>{{ date.label }}</strong> [label]="date.label" [time]="date.timestamp" [relativeTo]="date.relativeto" [title]="module.name" [url]="module.url">
{{ date.readableTime }} </core-reminders-date>
</p>
</div> </div>
<!-- Availability info space. --> <!-- Availability info space. -->

View File

@ -48,12 +48,6 @@
padding-top: 8px; padding-top: 8px;
} }
.core-module-dates ion-icon {
margin-left: 4px;
margin-right: 4px;
}
.core-module-dates,
.core-module-availabilityinfo { .core-module-availabilityinfo {
font-size: 90%; font-size: 90%;
ion-icon { ion-icon {
@ -94,8 +88,6 @@
white-space: normal !important; white-space: normal !important;
} }
} }
} }
:host-context(.core-iframe-fullscreen) { :host-context(.core-iframe-fullscreen) {

View File

@ -81,10 +81,9 @@
*ngIf="(showActivityDates && module.dates && module.dates.length) || module.availabilityinfo"> *ngIf="(showActivityDates && module.dates && module.dates.length) || module.availabilityinfo">
<!-- Activity dates. --> <!-- Activity dates. -->
<div *ngIf="showActivityDates && module.dates && module.dates.length" class="core-module-dates"> <div *ngIf="showActivityDates && module.dates && module.dates.length" class="core-module-dates">
<p *ngFor="let date of module.dates"> <core-reminders-date *ngFor="let date of module.dates" [type]="date.id" [label]="date.label" [time]="date.timestamp"
<ion-icon name="fas-calendar" aria-hidden="true"></ion-icon><strong>{{ date.label }}</strong> [relativeTo]="date.relativeto">
{{ date.readableTime }} </core-reminders-date>
</p>
</div> </div>
<!-- Availability info --> <!-- Availability info -->

View File

@ -57,16 +57,14 @@
</core-progress-bar> </core-progress-bar>
</div> </div>
<div *ngIf="course.startdate || course.enddate" class="core-course-dates"> <div *ngIf="course.startdate || course.enddate" class="core-course-dates">
<p *ngIf="course.startdate"> <core-reminders-date *ngIf="course.startdate" component="course" [instanceId]="course.id" type="coursestart"
<ion-icon name="fas-calendar" aria-hidden="true"></ion-icon> [label]="'core.course.startdate' | translate" [time]="course.startdate" [title]="course.fullname"
<strong>{{ 'core.course.startdate' | translate }}</strong><br> [url]="courseUrl">
{{ course.startdate * 1000 | coreFormatDate:'strftimedaydatetime' }} </core-reminders-date>
</p> <core-reminders-date *ngIf="course.enddate" component="course" [instanceId]="course.id" type="courseend"
<p *ngIf="course.enddate"> [label]="'core.course.enddate' | translate" [time]="course.enddate" [title]="course.fullname"
<ion-icon name="fas-calendar" aria-hidden="true"></ion-icon> [url]="courseUrl">
<strong>{{ 'core.course.enddate' | translate }}</strong><br> </core-reminders-date>
{{ course.enddate * 1000 | coreFormatDate:'strftimedaydatetime' }}
</p>
</div> </div>
</ion-label> </ion-label>
</ion-item> </ion-item>

View File

@ -17,6 +17,7 @@ import { RouterModule, Routes } from '@angular/router';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { CoreCourseSummaryPage } from './course-summary'; import { CoreCourseSummaryPage } from './course-summary';
import { CoreRemindersComponentsModule } from '@features/reminders/components/components.module';
const routes: Routes = [ const routes: Routes = [
{ {
@ -27,6 +28,7 @@ const routes: Routes = [
@NgModule({ @NgModule({
imports: [ imports: [
CoreSharedModule, CoreSharedModule,
CoreRemindersComponentsModule,
], ],
declarations: [ declarations: [
CoreCourseSummaryPage, CoreCourseSummaryPage,
@ -39,6 +41,7 @@ export class CoreCoursePreviewPageComponentModule { }
RouterModule.forChild(routes), RouterModule.forChild(routes),
CoreSharedModule, CoreSharedModule,
CoreCoursePreviewPageComponentModule, CoreCoursePreviewPageComponentModule,
CoreRemindersComponentsModule,
], ],
exports: [RouterModule], exports: [RouterModule],
}) })

View File

@ -27,7 +27,6 @@ import {
CoreCourseModuleCompletionTracking, CoreCourseModuleCompletionTracking,
CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionStatus,
CoreCourseGetContentsWSModule, CoreCourseGetContentsWSModule,
CoreCourseGetContentsWSModuleDate,
} from './course'; } from './course';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
@ -1189,7 +1188,7 @@ export class CoreCourseHelperProvider {
* This should be used in 3.6 sites or higher, where the course contents already include the completion. * This should be used in 3.6 sites or higher, where the course contents already include the completion.
* *
* @param courseId The course to get the completion. * @param courseId The course to get the completion.
* @param mmodule The module. * @param module The module.
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
@ -2086,20 +2085,12 @@ export type CoreCourseSectionWithStatus = CoreCourseSection & {
/** /**
* Module with calculated data. * Module with calculated data.
*/ */
export type CoreCourseModuleData = Omit<CoreCourseGetContentsWSModule, 'completiondata'|'dates'> & { export type CoreCourseModuleData = Omit<CoreCourseGetContentsWSModule, 'completiondata'> & {
course: number; // The course id. course: number; // The course id.
isStealth?: boolean; isStealth?: boolean;
handlerData?: CoreCourseModuleHandlerData; handlerData?: CoreCourseModuleHandlerData;
completiondata?: CoreCourseModuleCompletionData; completiondata?: CoreCourseModuleCompletionData;
section: number; section: number;
dates?: CoreCourseModuleDate[];
};
/**
* Module date with calculated data.
*/
export type CoreCourseModuleDate = CoreCourseGetContentsWSModuleDate & {
readableTime: string;
}; };
/** /**

View File

@ -38,7 +38,7 @@ import {
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreWSError } from '@classes/errors/wserror'; import { CoreWSError } from '@classes/errors/wserror';
import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
import { CoreCourseHelper, CoreCourseModuleData, CoreCourseModuleCompletionData, CoreCourseModuleDate } from './course-helper'; import { CoreCourseHelper, CoreCourseModuleData, CoreCourseModuleCompletionData } from './course-helper';
import { CoreCourseFormatDelegate } from './format-delegate'; import { CoreCourseFormatDelegate } from './format-delegate';
import { CoreCronDelegate } from '@services/cron'; import { CoreCronDelegate } from '@services/cron';
import { CoreCourseLogCronHandler } from './handlers/log-cron'; import { CoreCourseLogCronHandler } from './handlers/log-cron';
@ -53,7 +53,6 @@ import { CoreDatabaseTable } from '@classes/database/database-table';
import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy'; import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy';
import { SQLiteDB } from '@classes/sqlitedb'; import { SQLiteDB } from '@classes/sqlitedb';
import { CorePlatform } from '@services/platform'; import { CorePlatform } from '@services/platform';
import { CoreTime } from '@singletons/time';
import { asyncObservable, firstValueFrom } from '@/core/utils/rxjs'; import { asyncObservable, firstValueFrom } from '@/core/utils/rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -678,39 +677,11 @@ export class CoreCourseProvider {
}; };
} }
let formattedDates: CoreCourseModuleDate[] | undefined;
if (module.dates) {
formattedDates = module.dates.map(date => {
let readableTime = '';
if (!date.relativeto) {
readableTime = CoreTimeUtils.userDate(date.timestamp * 1000, 'core.strftimedatetime', true);
} else {
readableTime = Translate.instant(
'core.course.relativedatessubmissionduedate' + (date.timestamp > date.relativeto ? 'after' : 'before'),
{
$a: {
datediffstr: date.relativeto === date.timestamp ?
'0 ' + Translate.instant('core.secs') :
CoreTime.formatTime(date.relativeto - date.timestamp, 3),
},
},
);
}
return {
...date,
readableTime,
};
});
}
return { return {
...module, ...module,
course: courseId, course: courseId,
section: sectionId, section: sectionId,
completiondata: completionData, completiondata: completionData,
dates: formattedDates,
}; };
} }
@ -1775,7 +1746,10 @@ export type CoreCourseGetContentsWSModule = {
completiondata?: CoreCourseModuleWSCompletionData; // Module completion data. completiondata?: CoreCourseModuleWSCompletionData; // Module completion data.
contents?: CoreCourseModuleContentFile[]; contents?: CoreCourseModuleContentFile[];
downloadcontent?: number; // @since 4.0 The download content value. downloadcontent?: number; // @since 4.0 The download content value.
dates?: CoreCourseGetContentsWSModuleDate[]; // @since 3.11. Activity dates. dates?: {
label: string;
timestamp: number;
}[]; // @since 3.11. Activity dates.
contentsinfo?: { // @since v3.7.6 Contents summary information. contentsinfo?: { // @since v3.7.6 Contents summary information.
filescount: number; // Total number of files. filescount: number; // Total number of files.
filessize: number; // Total files size. filessize: number; // Total files size.
@ -1785,16 +1759,6 @@ export type CoreCourseGetContentsWSModule = {
}; };
}; };
/**
* Activity date.
*/
export type CoreCourseGetContentsWSModuleDate = {
label: string;
timestamp: number;
relativeto?: number; // @since 4.1. Relative date timestamp.
dataid?: string; // @since 4.1. ID to identify the text.
};
/** /**
* Data returned by core_course_get_contents WS. * Data returned by core_course_get_contents WS.
*/ */

View File

@ -14,20 +14,26 @@
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CoreRemindersDateComponent } from './date/date';
import { CoreRemindersSetButtonComponent } from './set-button/set-button';
import { CoreRemindersSetReminderCustomComponent } from './set-reminder-custom/set-reminder-custom'; import { CoreRemindersSetReminderCustomComponent } from './set-reminder-custom/set-reminder-custom';
import { CoreRemindersSetReminderMenuComponent } from './set-reminder-menu/set-reminder-menu'; import { CoreRemindersSetReminderMenuComponent } from './set-reminder-menu/set-reminder-menu';
@NgModule({ @NgModule({
declarations: [ declarations: [
CoreRemindersSetReminderMenuComponent, CoreRemindersDateComponent,
CoreRemindersSetButtonComponent,
CoreRemindersSetReminderCustomComponent, CoreRemindersSetReminderCustomComponent,
CoreRemindersSetReminderMenuComponent,
], ],
imports: [ imports: [
CoreSharedModule, CoreSharedModule,
], ],
exports: [ exports: [
CoreRemindersSetReminderMenuComponent, CoreRemindersDateComponent,
CoreRemindersSetButtonComponent,
CoreRemindersSetReminderCustomComponent, CoreRemindersSetReminderCustomComponent,
CoreRemindersSetReminderMenuComponent,
], ],
}) })
export class CoreRemindersComponentsModule {} export class CoreRemindersComponentsModule {}

View File

@ -0,0 +1,8 @@
<div>
<ion-icon name="fas-calendar" aria-hidden="true"></ion-icon>
<strong>{{ label }}</strong> {{ readableTime }}
</div>
<core-reminders-set-button *ngIf="showReminderButton" slot="end" [component]="component" [instanceId]="instanceId" [type]="type"
[label]="label" [timebefore]="timebefore" [time]="time" [title]="title" [url]="url">
</core-reminders-set-button>

View File

@ -0,0 +1,21 @@
@import "~theme/globals";
:host {
display: flex;
flex-direction: row;
}
div {
flex-grow: 1;
font-size: 14px;
margin: 0;
align-self: center;
ion-icon {
@include margin-horizontal(0px, 4px);
}
}
core-reminders-date + :host {
margin-top: 12px;
}

View File

@ -0,0 +1,95 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreReminders } from '@features/reminders/services/reminders';
import { Component, Input, OnInit } from '@angular/core';
import { CoreTimeUtils } from '@services/utils/time';
import { Translate } from '@singletons';
import { CoreTime } from '@singletons/time';
/**
* Component that displays a date to remind.
*/
@Component({
selector: 'core-reminders-date',
templateUrl: 'date.html',
styleUrls: ['date.scss'],
})
export class CoreRemindersDateComponent implements OnInit {
@Input() component?: string;
@Input() instanceId?: number;
@Input() type?: string;
@Input() label = '';
@Input() time = 0;
@Input() relativeTo = 0;
@Input() title = '';
@Input() url = '';
showReminderButton = false;
timebefore?: number; // Undefined means no reminder has been set.
readableTime = '';
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
this.readableTime = this.getReadableTime(this.time, this.relativeTo);
// If not set, button won't be shown.
if (this.component === undefined || this.instanceId === undefined || this.type === undefined) {
return;
}
const remindersEnabled = CoreReminders.isEnabled();
this.showReminderButton = remindersEnabled && this.time > CoreTimeUtils.timestamp();
if (!this.showReminderButton) {
return;
}
const reminders = await CoreReminders.getReminders({
instanceId: this.instanceId,
component: this.component,
type: this.type,
});
this.timebefore = reminders[0]?.timebefore;
}
/**
* Returns the readable time.
*
* @param timestamp Timestamp.
* @param relativeTo Base timestamp if timestamp is relative to this one.
* @return Readable time string.
*/
protected getReadableTime(timestamp: number, relativeTo = 0): string {
if (!relativeTo) {
return CoreTimeUtils.userDate(timestamp * 1000, 'core.strftimedatetime', true);
}
return Translate.instant(
'core.course.relativedatessubmissionduedate' + (timestamp > relativeTo ? 'after' : 'before'),
{
$a: {
datediffstr: relativeTo === timestamp ?
'0 ' + Translate.instant('core.secs') :
CoreTime.formatTime(relativeTo - timestamp, 3),
},
},
);
}
}

View File

@ -0,0 +1,4 @@
<ion-button fill="clear" size="small" (click)="setReminder($event)">
<ion-icon name="fas-bell" slot="icon-only" *ngIf="timebefore !== undefined"></ion-icon>
<ion-icon name="far-bell-slash" slot="icon-only" *ngIf="timebefore === undefined"></ion-icon>
</ion-button>

View File

@ -0,0 +1,116 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreReminderData, CoreReminders, CoreRemindersService } from '@features/reminders/services/reminders';
import { Component, Input } from '@angular/core';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreRemindersSetReminderMenuComponent } from '../set-reminder-menu/set-reminder-menu';
/**
* Component that displays a button to set a reminder.
*/
@Component({
selector: 'core-reminders-set-button',
templateUrl: 'set-button.html',
})
export class CoreRemindersSetButtonComponent {
@Input() component?: string;
@Input() instanceId?: number;
@Input() type?: string;
@Input() label = '';
@Input() timebefore?: number;
@Input() time = -1;
@Input() title = '';
@Input() url = '';
/**
* Set reminder.
*
* @param ev Click event.
*/
async setReminder(ev: Event): Promise<void> {
if (this.component === undefined || this.instanceId === undefined || this.type === undefined) {
return;
}
ev.preventDefault();
ev.stopPropagation();
if (this.timebefore === undefined) {
// Set it to the time of the event.
this.saveReminder(0);
return;
}
// Open popover.
const reminderTime = await CoreDomUtils.openPopover<{timeBefore: number}>({
component: CoreRemindersSetReminderMenuComponent,
componentProps: {
initialValue: this.timebefore,
noReminderLabel: 'core.reminders.delete',
},
event: ev,
});
if (reminderTime === undefined) {
// User canceled.
return;
}
// Save before.
this.saveReminder(reminderTime.timeBefore);
}
/**
* Save reminder.
*
* @param timebefore Time before the event to fire the notification.
* @return Promise resolved when done.
*/
protected async saveReminder(timebefore: number): Promise<void> {
if (this.component === undefined || this.instanceId === undefined || this.type === undefined) {
return;
}
if (timebefore === CoreRemindersService.DISABLED) {
// Remove the reminder.
await CoreReminders.removeReminders({
instanceId: this.instanceId,
component: this.component,
type: this.type,
});
this.timebefore = undefined;
return;
}
this.timebefore = timebefore;
const reminder: CoreReminderData = {
component: this.component,
instanceId: this.instanceId,
timebefore: this.timebefore,
type: this.type,
title: this.label + ' ' + this.title,
url: this.url,
time: this.time,
};
// Save before.
await CoreReminders.addReminder(reminder);
}
}