MOBILE-3936 reminders: Add reminders to courses and activities
parent
109d4bd2c5
commit
5d910ea5b4
|
@ -29,6 +29,7 @@ import { CoreCourseModuleManualCompletionComponent } from './module-manual-compl
|
|||
import { CoreCourseModuleNavigationComponent } from './module-navigation/module-navigation';
|
||||
import { CoreCourseModuleSummaryComponent } from './module-summary/module-summary';
|
||||
import { CoreCourseCourseIndexTourComponent } from './course-index-tour/course-index-tour';
|
||||
import { CoreRemindersComponentsModule } from '@features/reminders/components/components.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -48,6 +49,7 @@ import { CoreCourseCourseIndexTourComponent } from './course-index-tour/course-i
|
|||
],
|
||||
imports: [
|
||||
CoreBlockComponentsModule,
|
||||
CoreRemindersComponentsModule,
|
||||
CoreSharedModule,
|
||||
],
|
||||
exports: [
|
||||
|
|
|
@ -26,10 +26,9 @@
|
|||
|
||||
<!-- Activity dates. -->
|
||||
<div *ngIf="module.dates && module.dates.length" class="core-module-dates core-module-info-box-section">
|
||||
<p *ngFor="let date of module.dates">
|
||||
<ion-icon name="fas-calendar" aria-hidden="true"></ion-icon><strong>{{ date.label }}</strong>
|
||||
{{ date.readableTime }}
|
||||
</p>
|
||||
<core-reminders-date *ngFor="let date of module.dates" [component]="component" [instanceId]="module.id" [type]="date.dataid"
|
||||
[label]="date.label" [time]="date.timestamp" [relativeTo]="date.relativeto" [title]="module.name" [url]="module.url">
|
||||
</core-reminders-date>
|
||||
</div>
|
||||
|
||||
<!-- Availability info space. -->
|
||||
|
|
|
@ -48,12 +48,6 @@
|
|||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.core-module-dates ion-icon {
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.core-module-dates,
|
||||
.core-module-availabilityinfo {
|
||||
font-size: 90%;
|
||||
ion-icon {
|
||||
|
@ -94,8 +88,6 @@
|
|||
white-space: normal !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
:host-context(.core-iframe-fullscreen) {
|
||||
|
|
|
@ -81,10 +81,9 @@
|
|||
*ngIf="(showActivityDates && module.dates && module.dates.length) || module.availabilityinfo">
|
||||
<!-- Activity dates. -->
|
||||
<div *ngIf="showActivityDates && module.dates && module.dates.length" class="core-module-dates">
|
||||
<p *ngFor="let date of module.dates">
|
||||
<ion-icon name="fas-calendar" aria-hidden="true"></ion-icon><strong>{{ date.label }}</strong>
|
||||
{{ date.readableTime }}
|
||||
</p>
|
||||
<core-reminders-date *ngFor="let date of module.dates" [type]="date.id" [label]="date.label" [time]="date.timestamp"
|
||||
[relativeTo]="date.relativeto">
|
||||
</core-reminders-date>
|
||||
</div>
|
||||
|
||||
<!-- Availability info -->
|
||||
|
|
|
@ -57,16 +57,14 @@
|
|||
</core-progress-bar>
|
||||
</div>
|
||||
<div *ngIf="course.startdate || course.enddate" class="core-course-dates">
|
||||
<p *ngIf="course.startdate">
|
||||
<ion-icon name="fas-calendar" aria-hidden="true"></ion-icon>
|
||||
<strong>{{ 'core.course.startdate' | translate }}</strong><br>
|
||||
{{ course.startdate * 1000 | coreFormatDate:'strftimedaydatetime' }}
|
||||
</p>
|
||||
<p *ngIf="course.enddate">
|
||||
<ion-icon name="fas-calendar" aria-hidden="true"></ion-icon>
|
||||
<strong>{{ 'core.course.enddate' | translate }}</strong><br>
|
||||
{{ course.enddate * 1000 | coreFormatDate:'strftimedaydatetime' }}
|
||||
</p>
|
||||
<core-reminders-date *ngIf="course.startdate" component="course" [instanceId]="course.id" type="coursestart"
|
||||
[label]="'core.course.startdate' | translate" [time]="course.startdate" [title]="course.fullname"
|
||||
[url]="courseUrl">
|
||||
</core-reminders-date>
|
||||
<core-reminders-date *ngIf="course.enddate" component="course" [instanceId]="course.id" type="courseend"
|
||||
[label]="'core.course.enddate' | translate" [time]="course.enddate" [title]="course.fullname"
|
||||
[url]="courseUrl">
|
||||
</core-reminders-date>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
|
|
@ -17,6 +17,7 @@ import { RouterModule, Routes } from '@angular/router';
|
|||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreCourseSummaryPage } from './course-summary';
|
||||
import { CoreRemindersComponentsModule } from '@features/reminders/components/components.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -27,6 +28,7 @@ const routes: Routes = [
|
|||
@NgModule({
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
CoreRemindersComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreCourseSummaryPage,
|
||||
|
@ -39,6 +41,7 @@ export class CoreCoursePreviewPageComponentModule { }
|
|||
RouterModule.forChild(routes),
|
||||
CoreSharedModule,
|
||||
CoreCoursePreviewPageComponentModule,
|
||||
CoreRemindersComponentsModule,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
|
|
|
@ -27,7 +27,6 @@ import {
|
|||
CoreCourseModuleCompletionTracking,
|
||||
CoreCourseModuleCompletionStatus,
|
||||
CoreCourseGetContentsWSModule,
|
||||
CoreCourseGetContentsWSModuleDate,
|
||||
} from './course';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
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.
|
||||
*
|
||||
* @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.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
|
@ -2086,20 +2085,12 @@ export type CoreCourseSectionWithStatus = CoreCourseSection & {
|
|||
/**
|
||||
* Module with calculated data.
|
||||
*/
|
||||
export type CoreCourseModuleData = Omit<CoreCourseGetContentsWSModule, 'completiondata'|'dates'> & {
|
||||
export type CoreCourseModuleData = Omit<CoreCourseGetContentsWSModule, 'completiondata'> & {
|
||||
course: number; // The course id.
|
||||
isStealth?: boolean;
|
||||
handlerData?: CoreCourseModuleHandlerData;
|
||||
completiondata?: CoreCourseModuleCompletionData;
|
||||
section: number;
|
||||
dates?: CoreCourseModuleDate[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Module date with calculated data.
|
||||
*/
|
||||
export type CoreCourseModuleDate = CoreCourseGetContentsWSModuleDate & {
|
||||
readableTime: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -38,7 +38,7 @@ import {
|
|||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreWSError } from '@classes/errors/wserror';
|
||||
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 { CoreCronDelegate } from '@services/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 { SQLiteDB } from '@classes/sqlitedb';
|
||||
import { CorePlatform } from '@services/platform';
|
||||
import { CoreTime } from '@singletons/time';
|
||||
import { asyncObservable, firstValueFrom } from '@/core/utils/rxjs';
|
||||
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 {
|
||||
...module,
|
||||
course: courseId,
|
||||
section: sectionId,
|
||||
completiondata: completionData,
|
||||
dates: formattedDates,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1775,7 +1746,10 @@ export type CoreCourseGetContentsWSModule = {
|
|||
completiondata?: CoreCourseModuleWSCompletionData; // Module completion data.
|
||||
contents?: CoreCourseModuleContentFile[];
|
||||
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.
|
||||
filescount: number; // Total number of files.
|
||||
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.
|
||||
*/
|
||||
|
|
|
@ -14,20 +14,26 @@
|
|||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
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 { CoreRemindersSetReminderMenuComponent } from './set-reminder-menu/set-reminder-menu';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreRemindersSetReminderMenuComponent,
|
||||
CoreRemindersDateComponent,
|
||||
CoreRemindersSetButtonComponent,
|
||||
CoreRemindersSetReminderCustomComponent,
|
||||
CoreRemindersSetReminderMenuComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
],
|
||||
exports: [
|
||||
CoreRemindersSetReminderMenuComponent,
|
||||
CoreRemindersDateComponent,
|
||||
CoreRemindersSetButtonComponent,
|
||||
CoreRemindersSetReminderCustomComponent,
|
||||
CoreRemindersSetReminderMenuComponent,
|
||||
],
|
||||
})
|
||||
export class CoreRemindersComponentsModule {}
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue