MOBILE-3936 core: Add LocalNotifications mock
parent
43cec87112
commit
12c19080f2
|
@ -15,8 +15,8 @@
|
|||
iconAction="fas-th-list" (action)="toggleDisplay()"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="!showCalendar" [priority]="800" [content]="'addon.calendar.monthlyview' | translate"
|
||||
iconAction="fas-calendar-alt" (action)="toggleDisplay()"></core-context-menu-item>
|
||||
<core-context-menu-item [hidden]="!notificationsEnabled" [priority]="600" [content]="'core.settings.settings' | translate"
|
||||
(action)="openSettings()" iconAction="fas-cogs">
|
||||
<core-context-menu-item [priority]="600" [content]="'core.settings.settings' | translate" (action)="openSettings()"
|
||||
iconAction="fas-cogs">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item [hidden]="!loaded || !hasOffline || !isOnline" [priority]="400"
|
||||
[content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event, true)"
|
||||
|
|
|
@ -31,7 +31,6 @@ import { AddonCalendarCalendarComponent } from '../../components/calendar/calend
|
|||
import { AddonCalendarUpcomingEventsComponent } from '../../components/upcoming-events/upcoming-events';
|
||||
import { AddonCalendarFilterComponent } from '../../components/filter/filter';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreLocalNotifications } from '@services/local-notifications';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager';
|
||||
|
||||
|
@ -64,7 +63,6 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
|
|||
month?: number;
|
||||
canCreate = false;
|
||||
courses: Partial<CoreEnrolledCourseData>[] = [];
|
||||
notificationsEnabled = false;
|
||||
loaded = false;
|
||||
hasOffline = false;
|
||||
isOnline = false;
|
||||
|
@ -165,8 +163,6 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
|
|||
* View loaded.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.notificationsEnabled = CoreLocalNotifications.isAvailable();
|
||||
|
||||
this.loadUpcoming = !!CoreNavigator.getRouteBooleanParam('upcoming');
|
||||
this.showCalendar = !this.loadUpcoming;
|
||||
|
||||
|
|
|
@ -1384,10 +1384,6 @@ export class AddonCalendarProvider {
|
|||
async updateAllSitesEventReminders(): Promise<void> {
|
||||
await CorePlatform.ready();
|
||||
|
||||
if (!CoreLocalNotifications.isAvailable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const siteIds = await CoreSites.getSitesIds();
|
||||
|
||||
await Promise.all(siteIds.map((siteId: string) => async () => {
|
||||
|
@ -1430,11 +1426,6 @@ export class AddonCalendarProvider {
|
|||
events: ({ id: number; timestart: number; name: string})[],
|
||||
siteId: string,
|
||||
): Promise<void> {
|
||||
|
||||
if (!CoreLocalNotifications.isAvailable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(events.map(async (event) => {
|
||||
if (event.timestart * 1000 <= Date.now()) {
|
||||
// The event has already started, don't schedule it.
|
||||
|
|
|
@ -25,6 +25,7 @@ import { FileOpener } from '@ionic-native/file-opener/ngx';
|
|||
import { FileTransfer } from '@ionic-native/file-transfer/ngx';
|
||||
import { Geolocation } from '@ionic-native/geolocation/ngx';
|
||||
import { InAppBrowser } from '@ionic-native/in-app-browser/ngx';
|
||||
import { LocalNotifications } from '@ionic-native/local-notifications/ngx';
|
||||
import { MediaCapture } from '@ionic-native/media-capture/ngx';
|
||||
import { Zip } from '@ionic-native/zip/ngx';
|
||||
|
||||
|
@ -36,9 +37,11 @@ import { FileOpenerMock } from './services/file-opener';
|
|||
import { FileTransferMock } from './services/file-transfer';
|
||||
import { GeolocationMock } from './services/geolocation';
|
||||
import { InAppBrowserMock } from './services/inappbrowser';
|
||||
import { LocalNotificationsMock } from './services/local-notifications';
|
||||
import { MediaCaptureMock } from './services/media-capture';
|
||||
import { ZipMock } from './services/zip';
|
||||
import { CorePlatform } from '@services/platform';
|
||||
import { CoreLocalNotifications } from '@services/local-notifications';
|
||||
|
||||
/**
|
||||
* This module handles the emulation of Cordova plugins in browser and desktop.
|
||||
|
@ -90,6 +93,12 @@ import { CorePlatform } from '@services/platform';
|
|||
provide: Zip,
|
||||
useFactory: (): Zip => CorePlatform.is('cordova') ? new Zip() : new ZipMock(),
|
||||
},
|
||||
{
|
||||
provide: LocalNotifications,
|
||||
useFactory: (): LocalNotifications => CoreLocalNotifications.isPluginAvailable()
|
||||
? new LocalNotifications()
|
||||
: new LocalNotificationsMock(),
|
||||
},
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: () => () => {
|
||||
|
|
|
@ -0,0 +1,407 @@
|
|||
// (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 { CoreError } from '@classes/errors/error';
|
||||
import { ILocalNotification, ILocalNotificationAction, LocalNotifications } from '@ionic-native/local-notifications/ngx';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Mock LocalNotifications service.
|
||||
*/
|
||||
export class LocalNotificationsMock extends LocalNotifications {
|
||||
|
||||
protected scheduledNotifications: ILocalNotification[] = [];
|
||||
protected triggeredNotifications: ILocalNotification[] = [];
|
||||
protected presentNotifications: Record<number, Notification> = {};
|
||||
protected nextTimeout = 0;
|
||||
protected hasGranted?: boolean;
|
||||
protected observables = {
|
||||
trigger: new Subject<ILocalNotification>(),
|
||||
click: new Subject<ILocalNotification>(),
|
||||
clear: new Subject<Notification>(),
|
||||
clearall: new Subject<void>(),
|
||||
cancel: new Subject<ILocalNotification>(),
|
||||
cancelall: new Subject<void>(),
|
||||
schedule: new Subject<ILocalNotification>(),
|
||||
update: new Subject<ILocalNotification>(),
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
schedule(options?: ILocalNotification | Array<ILocalNotification>): void {
|
||||
this.hasPermission().then(() => {
|
||||
// Do not check permission here, it could be denied by Selenium.
|
||||
if (!options) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(options)) {
|
||||
options = [options];
|
||||
}
|
||||
|
||||
this.scheduledNotifications = this.scheduledNotifications.concat(options);
|
||||
this.scheduledNotifications.sort((a, b) =>
|
||||
(a.trigger?.at?.getTime() || 0) - (b.trigger?.at?.getTime() || 0));
|
||||
|
||||
options.forEach((notification) => {
|
||||
this.observables.schedule.next(notification);
|
||||
});
|
||||
|
||||
this.scheduleNotifications();
|
||||
|
||||
return;
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets timeout for next nofitication.
|
||||
*/
|
||||
protected scheduleNotifications(): void {
|
||||
window.clearTimeout(this.nextTimeout);
|
||||
|
||||
const nextNotification = this.scheduledNotifications[0];
|
||||
if (!nextNotification) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notificationTime = nextNotification.trigger?.at?.getTime() || 0;
|
||||
const timeout = notificationTime - Date.now();
|
||||
if (timeout <= 0) {
|
||||
this.triggerNextNotification();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.nextTimeout = window.setTimeout(() => {
|
||||
this.triggerNextNotification();
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the next notification.
|
||||
*/
|
||||
protected triggerNextNotification(): void {
|
||||
const dateNow = Date.now();
|
||||
|
||||
const nextNotification = this.scheduledNotifications[0];
|
||||
if (!nextNotification) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notificationTime = nextNotification.trigger?.at?.getTime() || 0;
|
||||
if (notificationTime === 0 || notificationTime <= dateNow) {
|
||||
const body = Array.isArray(nextNotification.text) ? nextNotification.text.join() : nextNotification.text;
|
||||
const notification = new Notification(nextNotification.title || '', {
|
||||
body,
|
||||
data: nextNotification.data,
|
||||
icon: nextNotification.icon,
|
||||
requireInteraction: true,
|
||||
tag: nextNotification.data?.component,
|
||||
});
|
||||
|
||||
this.triggeredNotifications.push(nextNotification);
|
||||
|
||||
this.observables.trigger.next(nextNotification);
|
||||
|
||||
notification.addEventListener('click', () => {
|
||||
this.observables.click.next(nextNotification);
|
||||
});
|
||||
|
||||
if (nextNotification.id) {
|
||||
this.presentNotifications[nextNotification.id] = notification;
|
||||
|
||||
notification.addEventListener('close', () => {
|
||||
delete(this.presentNotifications[nextNotification.id ?? 0]);
|
||||
});
|
||||
}
|
||||
|
||||
this.scheduledNotifications.shift();
|
||||
this.triggerNextNotification();
|
||||
} else {
|
||||
this.scheduleNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
update(options?: ILocalNotification): void {
|
||||
if (!options?.id) {
|
||||
return;
|
||||
}
|
||||
const index = this.scheduledNotifications.findIndex((notification) => notification.id === options.id);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.observables.update.next(options);
|
||||
|
||||
this.scheduledNotifications[index] = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async clear(notificationId: number | Array<number>): Promise<void> {
|
||||
if (!Array.isArray(notificationId)) {
|
||||
notificationId = [notificationId];
|
||||
}
|
||||
|
||||
notificationId.forEach((id) => {
|
||||
if (!this.presentNotifications[id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.presentNotifications[id].close();
|
||||
|
||||
this.observables.clear.next(this.presentNotifications[id]);
|
||||
delete this.presentNotifications[id];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async clearAll(): Promise<void> {
|
||||
for (const x in this.presentNotifications) {
|
||||
this.presentNotifications[x].close();
|
||||
}
|
||||
this.presentNotifications = {};
|
||||
|
||||
this.observables.clearall.next();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async cancel(notificationId: number | Array<number>): Promise<void> {
|
||||
if (!Array.isArray(notificationId)) {
|
||||
notificationId = [notificationId];
|
||||
}
|
||||
|
||||
notificationId.forEach((id) => {
|
||||
const index = this.scheduledNotifications.findIndex((notification) => notification.id === id);
|
||||
this.observables.cancel.next(this.scheduledNotifications[index]);
|
||||
|
||||
this.scheduledNotifications.splice(index, 1);
|
||||
});
|
||||
|
||||
this.scheduleNotifications();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async cancelAll(): Promise<void> {
|
||||
window.clearTimeout(this.nextTimeout);
|
||||
this.scheduledNotifications = [];
|
||||
|
||||
this.observables.cancelall.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isPresent(notificationId: number): Promise<boolean> {
|
||||
return !!this.presentNotifications[notificationId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isScheduled(notificationId: number): Promise<boolean> {
|
||||
return this.scheduledNotifications.some((notification) => notification.id === notificationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isTriggered(notificationId: number): Promise<boolean> {
|
||||
return this.triggeredNotifications.some((notification) => notification.id === notificationId);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getIds(): Promise<Array<number>> {
|
||||
const ids = await this.getScheduledIds();
|
||||
const triggeredIds = await this.getTriggeredIds();
|
||||
|
||||
return Promise.resolve(ids.concat(triggeredIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getTriggeredIds(): Promise<Array<number>> {
|
||||
const ids = this.triggeredNotifications
|
||||
.map((notification) => notification.id || 0)
|
||||
.filter((id) => id > 0);
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getScheduledIds(): Promise<Array<number>> {
|
||||
const ids = this.scheduledNotifications
|
||||
.map((notification) => notification.id || 0)
|
||||
.filter((id) => id > 0);
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async get(notificationId: number): Promise<ILocalNotification> {
|
||||
const notification = this.scheduledNotifications
|
||||
.find((notification) => notification.id === notificationId);
|
||||
|
||||
if (!notification) {
|
||||
throw new Error('Invalid Notification Id.');
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getAll(): Promise<Array<ILocalNotification>> {
|
||||
return this.scheduledNotifications.concat(this.triggeredNotifications);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getAllScheduled(): Promise<Array<ILocalNotification>> {
|
||||
return this.scheduledNotifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getAllTriggered(): Promise<Array<ILocalNotification>> {
|
||||
return this.triggeredNotifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async registerPermission(): Promise<boolean> {
|
||||
// We need to ask the user for permission
|
||||
const permission = await Notification.requestPermission();
|
||||
|
||||
this.hasGranted = permission === 'granted';
|
||||
|
||||
// If the user accepts, let's create a notification
|
||||
return this.hasGranted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async hasPermission(): Promise<boolean> {
|
||||
if (this.hasGranted !== undefined) {
|
||||
return this.hasGranted;
|
||||
}
|
||||
|
||||
if (!('Notification' in window)) {
|
||||
// Check if the browser supports notifications
|
||||
throw new CoreError('This browser does not support desktop notification');
|
||||
}
|
||||
|
||||
return this.registerPermission();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async addActions(groupId: unknown, actions: Array<ILocalNotificationAction>): Promise<Array<ILocalNotificationAction>> {
|
||||
// Not implemented.
|
||||
return actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async removeActions(groupId: unknown): Promise<unknown> {
|
||||
// Not implemented.
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async hasActions(groupId: unknown): Promise<boolean> {
|
||||
// Not implemented.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getDefaults(): Promise<unknown> {
|
||||
// Not implemented.
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async setDefaults(defaults: unknown): Promise<unknown> {
|
||||
// Not implemented.
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
on(eventName: string): Observable<unknown> {
|
||||
if (!this.observables[eventName]) {
|
||||
this.observables[eventName] = new Subject<ILocalNotification>();
|
||||
}
|
||||
|
||||
return this.observables[eventName];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
fireEvent(eventName: string, args: unknown): void {
|
||||
if (!this.observables[eventName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.observables[eventName].next(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async fireQueuedEvents(): Promise<unknown> {
|
||||
return this.triggerNextNotification();
|
||||
}
|
||||
|
||||
}
|
|
@ -473,11 +473,6 @@ export class CorePushNotificationsProvider {
|
|||
return this.notificationClicked(data);
|
||||
}
|
||||
|
||||
// If the app is in foreground when the notification is received, it's not shown. Let's show it ourselves.
|
||||
if (!CoreLocalNotifications.isAvailable()) {
|
||||
return this.notifyReceived(notification, data);
|
||||
}
|
||||
|
||||
const localNotif: ILocalNotification = {
|
||||
id: Number(data.notId) || 1,
|
||||
data: data,
|
||||
|
|
|
@ -67,7 +67,7 @@ export class CoreRemindersService {
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (!CoreLocalNotifications.isAvailable()) {
|
||||
if (!this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,7 @@ export class CoreRemindersService {
|
|||
* @return True if reminders are enabled and available, false otherwise.
|
||||
*/
|
||||
isEnabled(): boolean {
|
||||
return CoreLocalNotifications.isAvailable();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -314,7 +314,8 @@ export class CoreRemindersService {
|
|||
async scheduleAllNotifications(): Promise<void> {
|
||||
await CorePlatform.ready();
|
||||
|
||||
if (!this.isEnabled()) {
|
||||
if (CoreLocalNotifications.isPluginAvailable()) {
|
||||
// Notifications are already scheduled.
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ export class CoreSettingsDeviceInfoPage implements OnDestroy {
|
|||
lastCommit: CoreConstants.BUILD.lastCommitHash || '',
|
||||
networkStatus: CoreNetwork.isOnline() ? 'online' : 'offline',
|
||||
wifiConnection: CoreNetwork.isWifi() ? 'yes' : 'no',
|
||||
localNotifAvailable: CoreLocalNotifications.isAvailable() ? 'yes' : 'no',
|
||||
localNotifAvailable: CoreLocalNotifications.isPluginAvailable() ? 'yes' : 'no',
|
||||
pushId: CorePushNotifications.getPushId(),
|
||||
deviceType: '',
|
||||
};
|
||||
|
|
|
@ -319,14 +319,26 @@ export class CoreLocalNotificationsProvider {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns whether local notifications plugin is installed.
|
||||
* Returns whether local notifications are available.
|
||||
*
|
||||
* @return Whether local notifications plugin is installed.
|
||||
* @return Whether local notifications are available.
|
||||
* @deprecated since 4.1. It will always return true.
|
||||
*/
|
||||
isAvailable(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether local notifications plugin is available.
|
||||
*
|
||||
* @return Whether local notifications plugin is available.
|
||||
*/
|
||||
isPluginAvailable(): boolean {
|
||||
const win = <any> window; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
return !!win.cordova?.plugins?.notification?.local;
|
||||
const enabled = !!win.cordova?.plugins?.notification?.local;
|
||||
|
||||
return enabled && CorePlatform.is('cordova');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue