MOBILE-2328 notifications: Migrate Notifications

main
Albert Gasset 2018-03-27 13:27:55 +02:00
parent f3ea09c7ea
commit 133866598e
29 changed files with 1982 additions and 2 deletions

View File

@ -0,0 +1,34 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { NgModule } from '@angular/core';
import { AddonMessageOutputDelegate } from '@addon/messageoutput/providers/delegate';
import { AddonMessageOutputAirnotifierProvider } from './providers/airnotifier';
import { AddonMessageOutputAirnotifierHandler } from './providers/handler';
@NgModule({
declarations: [
],
imports: [
],
providers: [
AddonMessageOutputAirnotifierProvider,
AddonMessageOutputAirnotifierHandler,
]
})
export class AddonMessageOutputAirnotifierModule {
constructor(messageOutputDelegate: AddonMessageOutputDelegate, airnotifierHandler: AddonMessageOutputAirnotifierHandler) {
messageOutputDelegate.registerHandler(airnotifierHandler);
}
}

View File

@ -0,0 +1,3 @@
{
"processorsettingsdesc": "Configure devices"
}

View File

@ -0,0 +1,22 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'addon.messageoutput_airnotifier.processorsettingsdesc' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="devicesLoaded" (ionRefresh)="refreshDevices($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="devicesLoaded">
<ion-list>
<ion-item text-wrap *ngFor="let device of devices">
<ion-label [class.core-bold]="device.current">
{{ device.model }}
<span *ngIf="device.current">({{ 'core.currentdevice' | translate }})</span>
</ion-label>
<ion-spinner *ngIf="device.updating" item-end></ion-spinner>
<ion-toggle [disabled]="device.updating" [(ngModel)]="device.enable" (ngModelChange)="enableDevice(device, device.enable)"></ion-toggle>
</ion-item>
</ion-list>
</core-loading>
</ion-content>

View File

@ -0,0 +1,31 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { AddonMessageOutputAirnotifierDevicesPage } from './devices';
@NgModule({
declarations: [
AddonMessageOutputAirnotifierDevicesPage,
],
imports: [
CoreComponentsModule,
IonicPageModule.forChild(AddonMessageOutputAirnotifierDevicesPage),
TranslateModule.forChild()
],
})
export class AddonMessageOutputAirnotifierDevicesPageModule {}

View File

@ -0,0 +1,138 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Component, OnDestroy } from '@angular/core';
import { IonicPage } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { AddonPushNotificationsProvider } from '@addon/pushnotifications/providers/pushnotifications';
import { AddonMessageOutputAirnotifierProvider } from '../../providers/airnotifier';
/**
* Page that displays the list of devices.
*/
@IonicPage({ segment: 'addon-message-output-airnotifier-devices' })
@Component({
selector: 'page-addon-message-output-airnotifier-devices',
templateUrl: 'devices.html',
})
export class AddonMessageOutputAirnotifierDevicesPage implements OnDestroy {
title = '';
devices = [];
devicesLoaded = false;
protected updateTimeout: any;
constructor(private domUtils: CoreDomUtilsProvider, private airnotifierProivder: AddonMessageOutputAirnotifierProvider,
private pushNotificationsProvider: AddonPushNotificationsProvider ) {
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
this.fetchDevices();
}
/**
* Fetches the list of devices.
*
* @return {Promise<any>} Promise resolved when done.
*/
protected fetchDevices(): Promise<any> {
return this.airnotifierProivder.getUserDevices().then((devices) => {
const pushId = this.pushNotificationsProvider.getPushId();
// Convert enabled to boolean and search current device.
devices.forEach((device) => {
device.enable = !!device.enable;
device.current = pushId && pushId == device.pushid;
});
this.devices = devices;
}).catch((message) => {
this.domUtils.showErrorModal(message);
}).finally(() => {
this.devicesLoaded = true;
});
}
/**
* Update list of devices after a certain time. The purpose is to store the updated data, it won't be reflected in the view.
*/
protected updateDevicesAfterDelay(): void {
// Cancel pending updates.
if (this.updateTimeout) {
clearTimeout(this.updateTimeout);
}
this.updateTimeout = setTimeout(() => {
this.updateTimeout = null;
this.updateDevices();
}, 5000);
}
/**
* Fetch devices. The purpose is to store the updated data, it won't be reflected in the view.
*/
protected updateDevices(): void {
this.airnotifierProivder.invalidateUserDevices().finally(() => {
this.airnotifierProivder.getUserDevices();
});
}
/**
* Refresh the list of devices.
*
* @param {any} refresher Refresher.
*/
refreshDevices(refresher: any): void {
this.airnotifierProivder.invalidateUserDevices().finally(() => {
this.fetchDevices().finally(() => {
refresher.complete();
});
});
}
/**
* Enable or disable a certain device.
*
* @param {any} device
* @param {boolean} enable
*/
enableDevice(device: any, enable: boolean): void {
device.updating = true;
this.airnotifierProivder.enableDevice(device.id, enable).then(() => {
// Update the list of devices since it was modified.
this.updateDevicesAfterDelay();
}).catch((message) => {
// Show error and revert change.
this.domUtils.showErrorModal(message);
device.enable = !device.enable;
}).finally(() => {
device.updating = false;
});
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
// If there is a pending action to update devices, execute it right now.
if (this.updateTimeout) {
clearTimeout(this.updateTimeout);
this.updateDevices();
}
}
}

View File

@ -0,0 +1,115 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites';
import { CoreConfigConstants } from '../../../../configconstants';
/**
* Service to handle Airnotifier message output.
*/
@Injectable()
export class AddonMessageOutputAirnotifierProvider {
protected ROOT_CACHE_KEY = 'mmaMessageOutputAirnotifier:';
protected logger: any;
constructor(loggerProvider: CoreLoggerProvider, private sitesProvider: CoreSitesProvider) {
this.logger = loggerProvider.getInstance('AddonMessageOutputAirnotifier');
}
/**
* Enables or disables a device.
*
* @param {number} deviceId Device ID.
* @param {boolean} enable True to enable, false to disable.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if success.
*/
enableDevice(deviceId: number, enable: boolean, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const data = {
deviceid: deviceId,
enable: enable ? 1 : 0
};
return site.write('message_airnotifier_enable_device', data).then((result) => {
if (!result.success) {
// Fail. Reject with warning message if any.
if (result.warnings && result.warnings.length) {
return Promise.reject(result.warnings[0].message);
}
return Promise.reject(null);
}
});
});
}
/**
* Get the cache key for the get user devices call.
*
* @return {string} Cache key.
*/
protected getUserDevicesCacheKey(): string {
return this.ROOT_CACHE_KEY + 'userDevices';
}
/**
* Get user devices.
*
* @param {string} [siteid] Site ID. If not defined, use current site.
* @return {Promise<any>} Promise resolved with the devices.
*/
getUserDevices(siteId?: string): Promise<any> {
this.logger.debug('Get user devices');
return this.sitesProvider.getSite(siteId).then((site) => {
const data = {
appid: CoreConfigConstants.app_id
};
const preSets = {
cacheKey: this.getUserDevicesCacheKey()
};
return site.read('message_airnotifier_get_user_devices', data, preSets).then((data) => {
return data.devices;
});
});
}
/**
* Invalidate get user devices.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when data is invalidated.
*/
invalidateUserDevices(siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getUserDevicesCacheKey());
});
}
/**
* Returns whether or not the plugin is enabled for the current site.
*
* @return {boolean} True if enabled, false otherwise.
* @since 3.2
*/
isEnabled(): boolean {
return this.sitesProvider.wsAvailableInCurrentSite('message_airnotifier_enable_device') &&
this.sitesProvider.wsAvailableInCurrentSite('message_airnotifier_get_user_devices');
}
}

View File

@ -0,0 +1,52 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable } from '@angular/core';
import { AddonMessageOutputHandler, AddonMessageOutputHandlerData } from '@addon/messageoutput/providers/delegate';
import { AddonMessageOutputAirnotifierProvider } from './airnotifier';
/**
* Airnotifier message output handler.
*/
@Injectable()
export class AddonMessageOutputAirnotifierHandler implements AddonMessageOutputHandler {
name = 'AddonMessageOutputAirnotifier';
processorName = 'airnotifier';
constructor(private airnotifierProvider: AddonMessageOutputAirnotifierProvider) {}
/**
* Whether or not the module is enabled for the site.
*
* @return {boolean} True if enabled, false otherwise.
*/
isEnabled(): boolean {
return this.airnotifierProvider.isEnabled();
}
/**
* Returns the data needed to render the handler.
*
* @param {any} processor The processor object.
* @return {CoreMainMenuHandlerData} Data.
*/
getDisplayData(processor: any): AddonMessageOutputHandlerData {
return {
priority: 600,
label: 'addon.messageoutput_airnotifier.processorsettingsdesc',
icon: 'settings',
page: 'AddonMessageOutputAirnotifierDevicesPage',
};
}
}

View File

@ -0,0 +1,27 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { NgModule } from '@angular/core';
import { AddonMessageOutputDelegate } from './providers/delegate';
@NgModule({
declarations: [
],
imports: [
],
providers: [
AddonMessageOutputDelegate
]
})
export class AddonMessageOutputModule {}

View File

@ -0,0 +1,97 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable } from '@angular/core';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
import { CoreEventsProvider } from '@providers/events';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites';
/**
* Interface that all message output handlers must implement.
*/
export interface AddonMessageOutputHandler extends CoreDelegateHandler {
/**
* The name of the processor. E.g. 'airnotifier'.
* @type {string}
*/
processorName: string;
/**
* Returns the data needed to render the handler.
*
* @param {any} processor The processor object.
* @return {CoreMainMenuHandlerData} Data.
*/
getDisplayData(processor: any): AddonMessageOutputHandlerData;
}
/**
* Data needed to render a message output handler. It's returned by the handler.
*/
export interface AddonMessageOutputHandlerData {
/**
* Handler's priority.
* @type {number}
*/
priority: number;
/**
* Name of the page to load for the handler.
* @type {string}
*/
page: string;
/**
* Label to display for the handler.
* @type {string}
*/
label: string;
/**
* Name of the icon to display for the handler.
* @type {string}
*/
icon: string;
/**
* Params to pass to the page.
* @type {any}
*/
pageParams?: any;
}
/**
* Delegate to register processors (message/output) to be used in places like notification preferences.
*/
@Injectable()
export class AddonMessageOutputDelegate extends CoreDelegate {
protected handlerNameProperty = 'processorName';
constructor(protected loggerProvider: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider,
protected eventsProvider: CoreEventsProvider) {
super('CoreSettingsDelegate', loggerProvider, sitesProvider, eventsProvider);
}
/**
* Get the display data of the handler.
*
* @param {string} processor The processor object.
* @return {AddonMessageOutputHandlerData} Data.
*/
getDisplayData(processor: any): AddonMessageOutputHandlerData {
return this.executeFunctionOnEnabled(processor.name, 'getDisplayData', processor);
}
}

View File

@ -0,0 +1,8 @@
<ion-row *ngIf="actions && actions.length > 0">
<ion-col *ngFor="let action of actions">
<button ion-button icon-left clear small (click)="action.action()">
<ion-icon name="{{action.icon}}"></ion-icon>
{{ action.message | translate }}
</button>
</ion-col>
</ion-row>

View File

@ -0,0 +1,41 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Component, Input, OnInit } from '@angular/core';
import { CoreContentLinksDelegate, CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
/**
* Component that displays the actions for a notification.
*/
@Component({
selector: 'addon-notifications-actions',
templateUrl: 'actions.html',
})
export class AddonNotificationsActionsComponent implements OnInit {
@Input() contextUrl: string;
@Input() courseId: number;
actions: CoreContentLinksAction[] = [];
constructor(private contentLinksDelegate: CoreContentLinksDelegate) {}
/**
* Component being initialized.
*/
ngOnInit(): void {
this.contentLinksDelegate.getActionsFor(this.contextUrl, this.courseId).then((actions) => {
this.actions = actions;
});
}
}

View File

@ -0,0 +1,39 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { AddonNotificationsActionsComponent } from './actions/actions';
@NgModule({
declarations: [
AddonNotificationsActionsComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
],
providers: [
],
exports: [
AddonNotificationsActionsComponent
],
entryComponents: [
AddonNotificationsActionsComponent
]
})
export class AddonNotificationsComponentsModule {}

View File

@ -0,0 +1,7 @@
{
"errorgetnotifications": "Error getting notifications.",
"notificationpreferences": "Notification preferences",
"notifications": "Notifications",
"playsound": "Play sound",
"therearentnotificationsyet": "There are no notifications."
}

View File

@ -0,0 +1,80 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { NgModule } from '@angular/core';
import { AddonNotificationsProvider } from './providers/notifications';
import { AddonNotificationsMainMenuHandler } from './providers/mainmenu-handler';
import { AddonNotificationsSettingsHandler } from './providers/settings-handler';
import { AddonNotificationsCronHandler } from './providers/cron-handler';
import { CoreAppProvider } from '@providers/app';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate';
import { CoreSettingsDelegate } from '@core/settings/providers/delegate';
import { CoreCronDelegate } from '@providers/cron';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreSitesProvider } from '@providers/sites';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate';
@NgModule({
declarations: [
],
imports: [
],
providers: [
AddonNotificationsProvider,
AddonNotificationsMainMenuHandler,
AddonNotificationsSettingsHandler,
AddonNotificationsCronHandler,
]
})
export class AddonNotificationsModule {
constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: AddonNotificationsMainMenuHandler,
settingsDelegate: CoreSettingsDelegate, settingsHandler: AddonNotificationsSettingsHandler,
cronDelegate: CoreCronDelegate, cronHandler: AddonNotificationsCronHandler,
appProvider: CoreAppProvider, utils: CoreUtilsProvider, sitesProvider: CoreSitesProvider,
notificationsProvider: AddonNotificationsProvider, localNotifications: CoreLocalNotificationsProvider,
linkHelper: CoreContentLinksHelperProvider, pushNotificationsDelegate: AddonPushNotificationsDelegate) {
mainMenuDelegate.registerHandler(mainMenuHandler);
settingsDelegate.registerHandler(settingsHandler);
cronDelegate.register(cronHandler);
const notificationClicked = (notification: any): void => {
sitesProvider.isFeatureDisabled('CoreMainMenuDelegate_AddonNotifications', notification.site).then((disabled) => {
if (disabled) {
// Notifications are disabled, stop.
return;
}
notificationsProvider.invalidateNotificationsList().finally(() => {
linkHelper.goInSite(undefined, 'AddonNotificationsListPage', undefined, notification.site);
});
});
};
if (appProvider.isDesktop()) {
// Listen for clicks in simulated push notifications.
localNotifications.registerClick(AddonNotificationsProvider.PUSH_SIMULATION_COMPONENT, notificationClicked);
}
// Register push notification clicks.
pushNotificationsDelegate.on('click').subscribe((notification) => {
if (utils.isTrueOrOne(notification.notif)) {
notificationClicked(notification);
return true;
}
});
}
}

View File

@ -0,0 +1,30 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'addon.notifications.notifications' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="notificationsLoaded" (ionRefresh)="refreshNotifications($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="notificationsLoaded">
<ion-card *ngFor="let notification of notifications">
<ion-item>
<ion-avatar item-start core-user-link [userId]="notification.useridfrom" [courseId]="notification.courseid">
<img [src]="notification.profileimageurlfrom || 'assets/img/user-avatar.png'" core-external-content [alt]="'core.pictureof' | translate:{$a: notification.userfromfullname}" role="presentation">
</ion-avatar>
<h2>{{notification.userfromfullname}}</h2>
<div item-end *ngIf="!notification.timeread"><ion-icon name="record" color=""></ion-icon></div>
<p>{{notification.timecreated | coreDateDayOrTime}}</p>
</ion-item>
<ion-item text-wrap>
<p><core-format-text [text]="formatText(notification.mobiletext | coreCreateLinks)"></core-format-text></p>
</ion-item>
<addon-notifications-actions [contextUrl]="notification.contexturl" [courseId]="notification.courseid"></addon-notifications-actions>
</ion-card>
<core-empty-box *ngIf="!notifications || notifications.length <= 0" icon="notifications" [message]="'addon.notifications.therearentnotificationsyet' | translate"></core-empty-box>
<ion-infinite-scroll [enabled]="canLoadMore" (ionInfinite)="loadMoreNotifications($event)">
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
</core-loading>
</ion-content>

View File

@ -0,0 +1,37 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { AddonNotificationsComponentsModule } from '../../components/components.module';
import { AddonNotificationsListPage } from './list';
@NgModule({
declarations: [
AddonNotificationsListPage,
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
IonicPageModule.forChild(AddonNotificationsListPage),
TranslateModule.forChild(),
AddonNotificationsComponentsModule,
],
})
export class AddonNotificationsListPageModule {}

View File

@ -0,0 +1,192 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Component } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular';
import { Subscription } from 'rxjs';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreEventsProvider, CoreEventObserver } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { AddonNotificationsProvider } from '../../providers/notifications';
import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate';
/**
* Page that displays the list of notifications.
*/
@IonicPage({ segment: 'addon-notifications-list' })
@Component({
selector: 'page-addon-notifications-list',
templateUrl: 'list.html',
})
export class AddonNotificationsListPage {
notifications = [];
notificationsLoaded = false;
canLoadMore = false;
protected readCount = 0;
protected unreadCount = 0;
protected cronObserver: CoreEventObserver;
protected pushObserver: Subscription;
constructor(navParams: NavParams, private domUtils: CoreDomUtilsProvider, private eventsProvider: CoreEventsProvider,
private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider,
private utils: CoreUtilsProvider, private notificationsProvider: AddonNotificationsProvider,
private pushNotificationsDelegate: AddonPushNotificationsDelegate) {
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
this.fetchNotifications().finally(() => {
this.notificationsLoaded = true;
});
this.cronObserver = this.eventsProvider.on(AddonNotificationsProvider.READ_CRON_EVENT, () => this.refreshNotifications(),
this.sitesProvider.getCurrentSiteId());
this.pushObserver = this.pushNotificationsDelegate.on('receive').subscribe((notification) => {
// New notification received. If it's from current site, refresh the data.
if (this.utils.isTrueOrOne(notification.notif) && this.sitesProvider.isCurrentSite(notification.site)) {
this.refreshNotifications();
}
});
}
/**
* Convenience function to get notifications. Gets unread notifications first.
*
* @param {boolean} refreh
* @return {Primise<any>} Resolved when done.
*/
protected fetchNotifications(refresh?: boolean): Promise<any> {
if (refresh) {
this.readCount = 0;
this.unreadCount = 0;
}
const limit = AddonNotificationsProvider.LIST_LIMIT;
return this.notificationsProvider.getUnreadNotifications(this.unreadCount, limit).then((unread) => {
let promise;
/* Don't add the unread notifications to this.notifications yet. If there are no unread notifications
that causes that the "There are no notifications" message is shown in pull to refresh. */
this.unreadCount += unread.length;
if (unread.length < limit) {
// Limit not reached. Get read notifications until reach the limit.
const readLimit = limit - unread.length;
promise = this.notificationsProvider.getReadNotifications(this.readCount, readLimit).then((read) => {
this.readCount += read.length;
if (refresh) {
this.notifications = unread.concat(read);
} else {
this.notifications = this.notifications.concat(unread, read);
}
this.canLoadMore = read.length >= readLimit;
}).catch((error) => {
if (unread.length == 0) {
this.domUtils.showErrorModalDefault(error, 'addon.notifications.errorgetnotifications', true);
this.canLoadMore = false; // Set to false to prevent infinite calls with infinite-loading.
}
});
} else {
promise = Promise.resolve();
if (refresh) {
this.notifications = unread;
} else {
this.notifications = this.notifications.concat(unread);
}
this.canLoadMore = true;
}
return promise.then(() => {
// Mark retrieved notifications as read if they are not.
this.markNotificationsAsRead(unread);
});
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.notifications.errorgetnotifications', true);
this.canLoadMore = false; // Set to false to prevent infinite calls with infinite-loading.
});
}
/**
* Mark notifications as read.
*
* @param {any[]} notifications
*/
protected markNotificationsAsRead(notifications: any[]): void {
if (notifications.length > 0) {
const promises = notifications.map((notification) => {
return this.notificationsProvider.markNotificationRead(notification.id);
});
Promise.all(promises).finally(() => {
this.notificationsProvider.invalidateNotificationsList().finally(() => {
const siteId = this.sitesProvider.getCurrentSiteId();
this.eventsProvider.trigger(AddonNotificationsProvider.READ_CHANGED_EVENT, null, siteId);
});
});
}
}
/**
* Refresh notifications.
*
* @param {any} [refresher] Refresher.
*/
refreshNotifications(refresher?: any): void {
this.notificationsProvider.invalidateNotificationsList().finally(() => {
return this.fetchNotifications(true).finally(() => {
if (refresher) {
refresher.complete();
}
});
});
}
/**
* Load more results.
*
* @param {any} infiniteScroll The infinit scroll instance.
*/
loadMoreNotifications(infiniteScroll: any): void {
this.fetchNotifications().finally(() => {
infiniteScroll.complete();
});
}
/**
* Formats the text of a notification.
* @param {string} text
* @return {string} Formatted text.
*/
formatText(text: string): string {
text = text.replace(/-{4,}/ig, '');
text = this.textUtils.replaceNewLines(text, '<br>');
return text;
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
this.cronObserver && this.cronObserver.off();
this.pushObserver && this.pushObserver.unsubscribe();
}
}

View File

@ -0,0 +1,76 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'addon.notifications.notificationpreferences' | translate }}</ion-title>
<ion-buttons end>
</ion-buttons>
</ion-navbar>
</ion-header>
<core-navbar-buttons>
<core-context-menu *ngIf="processorHandlers.length > 0">
<core-context-menu-item *ngFor="let handler of processorHandlers" [priority]="handler.priority" [content]="handler.label | translate" (action)="openExtraPreferences(handler)" [iconAction]="handler.icon"></core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<ion-content>
<ion-refresher [enabled]="preferencesLoaded && notifPrefsEnabled" (ionRefresh)="refreshPreferences($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="preferencesLoaded">
<!-- If notification preferences aren't enabled, show only the notification sound setting. -->
<ion-item *ngIf="canChangeSound && !notifPrefsEnabled">
<ion-label>{{ 'addon.notifications.playsound' | translate }}</ion-label>
<ion-toggle [(ngModel)]="notificationSound" (ionChange)="changeNotificationSound(notificationSound)"></ion-toggle>
</ion-item>
<ng-container *ngIf="notifPrefsEnabled">
<ion-card>
<ion-item text-wrap *ngIf="preferences">
<ion-label>{{ 'core.settings.disableall' | translate }}</ion-label>
<ion-toggle [(ngModel)]="preferences.disableall" (ionChange)="disableAll(preferences.disableall)"></ion-toggle>
</ion-item>
<ion-item text-wrap *ngIf="canChangeSound">
<ion-label>{{ 'addon.notifications.playsound' | translate }}</ion-label>
<ion-toggle [(ngModel)]="notificationSound" (ionChange)="changeNotificationSound(notificationSound)"></ion-toggle>
</ion-item>
</ion-card>
<!-- Show processor selector. -->
<ion-select *ngIf="preferences && preferences.processors && preferences.processors.length > 0" [ngModel]="currentProcessor.name" (ngModelChange)="changeProcessor($event)" interface="popover">
<ion-option *ngFor="let processor of preferences.processors" [value]="processor.name">{{ processor.displayname }}</ion-option>
</ion-select>
<ion-card list *ngFor="let component of components">
<ion-item-divider color="light" text-wrap>
<ion-row no-padding>
<ion-col no-padding>{{ component.displayname }}</ion-col>
<ion-col col-2 text-center no-padding class="hidden-phone">{{ 'core.settings.loggedin' | translate }}</ion-col>
<ion-col col-2 text-center no-padding class="hidden-phone">{{ 'core.settings.loggedoff' | translate }}</ion-col>
</ion-row>
</ion-item-divider>
<ng-container *ngFor="let notification of component.notifications">
<!-- Tablet view -->
<ion-row text-wrap class="hidden-phone" align-items-center>
<ion-col margin-horizontal>{{ notification.displayname }}</ion-col>
<ion-col col-2 text-center *ngFor="let state of ['loggedin', 'loggedoff']">
<!-- If notifications not disabled, show toggle. -->
<ion-spinner [hidden]="preferences.disableall || !(notification.currentProcessor[state] && notification.currentProcessor[state].updating)"></ion-spinner>
<ion-toggle *ngIf="!preferences.disableall" [(ngModel)]="notification.currentProcessor[state].checked" (ionChange)="changePreference(notification, state)" [disabled]="notification.currentProcessor.locked || notification.currentProcessor[state].updating">
</ion-toggle>
<!-- If notifications are disabled, show "Disabled" instead of toggle. -->
<span *ngIf="preferences.disableall">{{ 'core.settings.disabled' | translate }}</span>
</ion-col>
</ion-row>
<!-- Phone view -->
<ion-list-header text-wrap no-margin class="hidden-tablet">{{ notification.displayname }}</ion-list-header>
<!-- If notifications not disabled, show toggles. If notifications are disabled, show "Disabled" instead of toggle. -->
<ion-item *ngFor="let state of ['loggedin', 'loggedoff']" text-wrap class="hidden-tablet">
<ion-label>{{ 'core.settings.' + state | translate }}</ion-label>
<ion-spinner item-end *ngIf="!preferences.disableall && (notification.currentProcessor[state] && notification.currentProcessor[state].updating)"></ion-spinner>
<ion-toggle item-end *ngIf="!preferences.disableall" [(ngModel)]="notification.currentProcessor[state].checked" (ionChange)="changePreference(notification, state)" [disabled]="notification.currentProcessor.locked || notification.currentProcessor[state].updating">
</ion-toggle>
<ion-note item-end *ngIf="preferences.disableall">{{ 'core.settings.disabled' | translate }}</ion-note>
</ion-item>
</ng-container>
</ion-card>
</ng-container>
</core-loading>
</ion-content>

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { AddonNotificationsSettingsPage } from './settings';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
AddonNotificationsSettingsPage,
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
IonicPageModule.forChild(AddonNotificationsSettingsPage),
TranslateModule.forChild()
],
})
export class AddonNotificationsSettingsPageModule {}

View File

@ -0,0 +1,265 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Component, OnDestroy, Optional } from '@angular/core';
import { IonicPage, NavController } from 'ionic-angular';
import { AddonNotificationsProvider } from '../../providers/notifications';
import { CoreUserProvider } from '@core/user/providers/user';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreSettingsHelper } from '@core/settings/providers/helper';
import { AddonMessageOutputDelegate, AddonMessageOutputHandlerData } from '@addon/messageoutput/providers/delegate';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreConfigProvider } from '@providers/config';
import { CoreAppProvider } from '@providers/app';
import { CoreConstants } from '@core/constants';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
/**
* Page that displays notifications settings.
*/
@IonicPage({ segment: 'addon-notifications-settings' })
@Component({
selector: 'page-addon-notifications-settings',
templateUrl: 'settings.html',
})
export class AddonNotificationsSettingsPage implements OnDestroy {
protected updateTimeout: any;
components: any[];
preferences: any;
preferencesLoaded: boolean;
currentProcessor: any;
notifPrefsEnabled: boolean;
canChangeSound: boolean;
notificationSound: boolean;
processorHandlers = [];
constructor(private notificationsProvider: AddonNotificationsProvider, private domUtils: CoreDomUtilsProvider,
private settingsHelper: CoreSettingsHelper, private userProvider: CoreUserProvider,
private navCtrl: NavController, private messageOutputDelegate: AddonMessageOutputDelegate,
appProvider: CoreAppProvider, private configProvider: CoreConfigProvider, private eventsProvider: CoreEventsProvider,
private localNotificationsProvider: CoreLocalNotificationsProvider, private sitesProvider: CoreSitesProvider,
@Optional() private svComponent: CoreSplitViewComponent) {
this.notifPrefsEnabled = notificationsProvider.isNotificationPreferencesEnabled();
this.canChangeSound = localNotificationsProvider.isAvailable() && !appProvider.isDesktop();
if (this.canChangeSound) {
configProvider.get(CoreConstants.SETTINGS_NOTIFICATION_SOUND, true).then((enabled) => {
this.notificationSound = enabled;
});
}
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
if (this.notifPrefsEnabled) {
this.fetchPreferences();
} else {
this.preferencesLoaded = true;
}
}
/**
* Fetches preference data.
*
* @return {Promise<any>} Resolved when done.
*/
protected fetchPreferences(): Promise<any> {
return this.notificationsProvider.getNotificationPreferences().then((preferences) => {
if (!this.currentProcessor) {
// Initialize current processor. Load "Mobile" (airnotifier) if available.
this.currentProcessor = this.settingsHelper.getProcessor(preferences.processors, 'airnotifier');
}
if (!this.currentProcessor) {
// Shouldn't happen.
return Promise.reject('No processor found');
}
preferences.disableall = !!preferences.disableall; // Convert to boolean.
this.preferences = preferences;
this.loadProcessor(this.currentProcessor);
// Get display data of message output handlers (thery are displayed in the context menu),
this.processorHandlers = [];
if (preferences.processors) {
preferences.processors.forEach((processor) => {
processor.supported = this.messageOutputDelegate.hasHandler(processor.name, true);
if (processor.hassettings && processor.supported) {
this.processorHandlers.push(this.messageOutputDelegate.getDisplayData(processor));
}
});
}
}).catch((message) => {
this.domUtils.showErrorModal(message);
}).finally(() => {
this.preferencesLoaded = true;
});
}
/**
* Load a processor.
*
* @param {any} processor
*/
protected loadProcessor(processor: any): void {
if (!processor) {
return;
}
this.currentProcessor = processor;
this.components = this.settingsHelper.getProcessorComponents(processor.name, this.preferences.components);
}
/**
* Update preferences after a certain time. The purpose is to store the updated data, it won't be reflected in the view.
*/
protected updatePreferencesAfterDelay(): void {
// Cancel pending updates.
clearTimeout(this.updateTimeout);
this.updateTimeout = setTimeout(() => {
this.updateTimeout = null;
this.updatePreferences();
}, 5000);
}
/**
* Update preferences. The purpose is to store the updated data, it won't be reflected in the view.
*/
protected updatePreferences(): void {
this.notificationsProvider.invalidateNotificationPreferences().finally(() => {
this.notificationsProvider.getNotificationPreferences();
});
}
/**
* The selected processor was changed.
*
* @param {string} name Name of the selected processor.
*/
changeProcessor(name: string): void {
this.preferences.processors.forEach((processor) => {
if (processor.name == name) {
this.loadProcessor(processor);
}
});
}
/**
* Refresh the list of preferences.
*
* @param {any} [refresher] Refresher.
*/
refreshPreferences(refresher?: any): void {
this.notificationsProvider.invalidateNotificationPreferences().finally(() => {
this.fetchPreferences().finally(() => {
refresher && refresher.complete();
});
});
}
/**
* Open extra preferences.
*
* @param {AddonMessageOutputHandlerData} handlerData
*/
openExtraPreferences(handlerData: AddonMessageOutputHandlerData): void {
// Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav.
const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
navCtrl.push(handlerData.page, handlerData.pageParams);
}
/**
* Change the value of a certain preference.
*
* @param {any} notification Notification object.
* @param {string} state State name, ['loggedin', 'loggedoff'].
*/
changePreference(notification: any, state: string): void {
const processorState = notification.currentProcessor[state];
const preferenceName = notification.preferencekey + '_' + processorState.name;
let value;
notification.processors.forEach((processor) => {
if (processor[state].checked) {
if (!value) {
value = processor.name;
} else {
value += ',' + processor.name;
}
}
});
if (!value) {
value = 'none';
}
processorState.updating = true;
this.userProvider.updateUserPreference(preferenceName, value).then(() => {
// Update the preferences since they were modified.
this.updatePreferencesAfterDelay();
}).catch((message) => {
// Show error and revert change.
this.domUtils.showErrorModal(message);
notification.currentProcessor[state].checked = !notification.currentProcessor[state].checked;
}).finally(() => {
processorState.updating = false;
});
}
/**
* Disable all notifications changed.
*/
disableAll(disable: boolean): void {
const modal = this.domUtils.showModalLoading('core.sending', true);
this.userProvider.updateUserPreferences([], disable).then(() => {
// Update the preferences since they were modified.
this.updatePreferencesAfterDelay();
}).catch((message) => {
// Show error and revert change.
this.domUtils.showErrorModal(message);
this.preferences.disableall = !this.preferences.disableall;
}).finally(() => {
modal.dismiss();
});
}
/**
* Change the notification sound setting.
*
* @param {enabled} enabled
*/
changeNotificationSound(enabled: boolean): void {
this.configProvider.set(CoreConstants.SETTINGS_NOTIFICATION_SOUND, enabled).finally(() => {
const siteId = this.sitesProvider.getCurrentSiteId();
this.eventsProvider.trigger(CoreEventsProvider.NOTIFICATION_SOUND_CHANGED, {enabled}, siteId);
this.localNotificationsProvider.rescheduleAll();
});
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
// If there is a pending action to update preferences, execute it right now.
if (this.updateTimeout) {
clearTimeout(this.updateTimeout);
this.updatePreferences();
}
}
}

View File

@ -0,0 +1,84 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable } from '@angular/core';
import { CoreAppProvider } from '@providers/app';
import { CoreCronHandler } from '@providers/cron';
import { CoreEventsProvider } from '@providers/events';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreSitesProvider } from '@providers/sites';
import { AddonNotificationsProvider } from './notifications';
/**
* Notifications cron handler.
*/
@Injectable()
export class AddonNotificationsCronHandler implements CoreCronHandler {
name = 'AddonNotificationsCronHandler';
constructor(private appProvider: CoreAppProvider, private eventsProvider: CoreEventsProvider,
private sitesProvider: CoreSitesProvider, private localNotifications: CoreLocalNotificationsProvider,
private notificationsProvider: AddonNotificationsProvider) {}
/**
* Get the time between consecutive executions.
*
* @return {number} Time between consecutive executions (in ms).
*/
getInterval(): number {
return this.appProvider.isDesktop() ? 60000 : 600000; // 1 or 10 minutes.
}
/**
* Check whether it's a synchronization process or not. True if not defined.
*
* @return {boolean} Whether it's a synchronization process or not.
*/
isSync(): boolean {
// This is done to use only wifi if using the fallback function.
// In desktop it is always sync, since it fetches notification to see if there's a new one.
return !this.notificationsProvider.isPreciseNotificationCountEnabled() || this.appProvider.isDesktop();
}
/**
* Check whether the sync can be executed manually. Call isSync if not defined.
*
* @return {boolean} Whether the sync can be executed manually.
*/
canManualSync(): boolean {
return true;
}
/**
* Execute the process.
*
* @param {string} [siteId] ID of the site affected. If not defined, all sites.
* @return {Promise<any>} Promise resolved when done. If the promise is rejected, this function will be called again often,
* it shouldn't be abused.
*/
execute(siteId?: string): Promise<any> {
if (this.sitesProvider.isCurrentSite(siteId)) {
this.eventsProvider.trigger(AddonNotificationsProvider.READ_CRON_EVENT, {}, this.sitesProvider.getCurrentSiteId());
}
if (this.appProvider.isDesktop() && this.localNotifications.isAvailable()) {
/* @todo
$mmEmulatorHelper.checkNewNotifications(
mmaNotificationsPushSimulationComponent, fetchNotifications, getTitleAndText, siteId);
*/
}
return Promise.resolve(null);
}
}

View File

@ -0,0 +1,115 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable } from '@angular/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/providers/delegate';
import { AddonNotificationsProvider } from './notifications';
import { AddonPushNotificationsProvider } from '@addon/pushnotifications/providers/pushnotifications';
import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate';
/**
* Handler to inject an option into main menu.
*/
@Injectable()
export class AddonNotificationsMainMenuHandler implements CoreMainMenuHandler {
name = 'AddonNotifications';
priority = 700;
protected handler: CoreMainMenuHandlerData = {
icon: 'notifications',
title: 'addon.notifications.notifications',
page: 'AddonNotificationsListPage',
class: 'addon-notifications-handler',
showBadge: true,
badge: '',
loading: true,
};
constructor(eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider,
utils: CoreUtilsProvider, private notificationsProvider: AddonNotificationsProvider,
private pushNotificationsProvider: AddonPushNotificationsProvider,
pushNotificationsDelegate: AddonPushNotificationsDelegate) {
eventsProvider.on(AddonNotificationsProvider.READ_CHANGED_EVENT, (data) => {
this.updateBadge(data.siteId);
});
eventsProvider.on(AddonNotificationsProvider.READ_CRON_EVENT, (data) => {
this.updateBadge(data.siteId);
});
// Reset info on logout.
eventsProvider.on(CoreEventsProvider.LOGOUT, (data) => {
this.handler.badge = '';
this.handler.loading = true;
});
// If a push notification is received, refresh the count.
pushNotificationsDelegate.on('receive').subscribe((notification) => {
// New notification received. If it's from current site, refresh the data.
if (utils.isTrueOrOne(notification.notif) && this.sitesProvider.isCurrentSite(notification.site)) {
this.updateBadge(notification.site);
}
});
// Register Badge counter.
pushNotificationsDelegate.registerCounterHandler('AddonNotifications');
}
/**
* Check if the handler is enabled on a site level.
*
* @return {boolean} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return true;
}
/**
* Returns the data needed to render the handler.
*
* @return {CoreMainMenuHandlerData} Data needed to render the handler.
*/
getDisplayData(): CoreMainMenuHandlerData {
if (this.handler.loading) {
this.updateBadge();
}
return this.handler;
}
/**
* Triggers an update for the badge number and loading status. Mandatory if showBadge is enabled.
*
* @param {string} siteId Site ID or current Site if undefined.
*/
updateBadge(siteId?: string): void {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
if (!siteId) {
return;
}
this.notificationsProvider.getUnreadNotificationsCount(null, siteId).then((unread) => {
this.handler.badge = unread > 0 ? String(unread) : '';
this.pushNotificationsProvider.updateAddonCounter('AddonNotifications', unread, siteId);
}).catch(() => {
this.handler.badge = '';
}).finally(() => {
this.handler.loading = false;
});
}
}

View File

@ -0,0 +1,291 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable } from '@angular/core';
import { CoreAppProvider } from '@providers/app';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUserProvider } from '@core/user/providers/user';
/**
* Service to handle notifications.
*/
@Injectable()
export class AddonNotificationsProvider {
static READ_CHANGED_EVENT = 'addon_notifications_read_changed_event';
static READ_CRON_EVENT = 'addon_notifications_read_cron_event';
static PUSH_SIMULATION_COMPONENT = 'AddonMessagesPushSimulation';
static LIST_LIMIT = 20;
protected ROOT_CACHE_KEY = 'mmaNotifications:';
protected logger;
constructor(logger: CoreLoggerProvider, private appProvider: CoreAppProvider, private sitesProvider: CoreSitesProvider,
private timeUtils: CoreTimeUtilsProvider, private userProvider: CoreUserProvider) {
this.logger = logger.getInstance('AddonNotificationsProvider');
}
/**
* Function to format notification data.
*
* @param {any[]} notifications List of notifications.
*/
protected formatNotificationsData(notifications: any[]): void {
notifications.forEach((notification) => {
// Set message to show.
if (notification.contexturl && notification.contexturl.indexOf('/mod/forum/')) {
notification.mobiletext = notification.smallmessage;
} else {
notification.mobiletext = notification.fullmessage;
}
// Try to set courseid the notification belongs to.
const cid = notification.fullmessagehtml.match(/course\/view\.php\?id=([^"]*)/);
if (cid && cid[1]) {
notification.courseid = cid[1];
}
// Try to get the profile picture of the user.
this.userProvider.getProfile(notification.useridfrom, notification.courseid, true).then((user) => {
notification.profileimageurlfrom = user.profileimageurl;
});
});
}
/**
* Get the cache key for the get notification preferences call.
*
* @return {string} Cache key.
*/
protected getNotificationPreferencesCacheKey(): string {
return this.ROOT_CACHE_KEY + 'notificationPreferences';
}
/**
* Get notification preferences.
*
* @param {string} [siteid] Site ID. If not defined, use current site.
* @return {Promise<any>} Promise resolved with the notification preferences.
*/
getNotificationPreferences(siteId?: string): Promise<any> {
this.logger.debug('Get notification preferences');
return this.sitesProvider.getSite(siteId).then((site) => {
const preSets = {
cacheKey: this.getNotificationPreferencesCacheKey()
};
return site.read('core_message_get_user_notification_preferences', {}, preSets).then((data) => {
return data.preferences;
});
});
}
/**
* Get cache key for notification list WS calls.
*
* @return {string} Cache key.
*/
protected getNotificationsCacheKey(): string {
return this.ROOT_CACHE_KEY + 'list';
}
/**
* Get notifications from site.
*
* @param {boolean} read True if should get read notifications, false otherwise.
* @param {number} limitFrom Position of the first notification to get.
* @param {number} limitNumber Number of notifications to get or 0 to use the default limit.
* @param {boolean} [toDisplay=true] True if notifications will be displayed to the user, either in view or in a notification.
* @param {boolean} [forceCache] True if it should return cached data. Has priority over ignoreCache.
* @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. If not defined, use current site.
* @return {Promise<any[]>} Promise resolved with notifications.
*/
getNotifications(read: boolean, limitFrom: number, limitNumber: number = 0, toDisplay: boolean = true,
forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise<any[]> {
limitNumber = limitNumber || AddonNotificationsProvider.LIST_LIMIT;
this.logger.debug('Get ' + (read ? 'read' : 'unread') + ' notifications from ' + limitFrom + '. Limit: ' + limitNumber);
return this.sitesProvider.getSite(siteId).then((site) => {
const data = {
useridto: site.getUserId(),
useridfrom: 0,
type: 'notifications',
read: read ? 1 : 0,
newestfirst: 1,
limitfrom: limitFrom,
limitnum: limitNumber
};
const preSets: object = {
cacheKey: this.getNotificationsCacheKey(),
omitExpires: forceCache,
getFromCache: forceCache || !ignoreCache,
emergencyCache: forceCache || !ignoreCache,
};
// Get unread notifications.
return site.read('core_message_get_messages', data, preSets).then((response) => {
if (response.messages) {
const notifications = response.messages;
this.formatNotificationsData(notifications);
if (this.appProvider.isDesktop() && toDisplay && !read && limitFrom === 0) {
/* @todo
// Store the last received notification. Don't block the user for this.
$mmEmulatorHelper.storeLastReceivedNotification(
mmaNotificationsPushSimulationComponent, notifications[0], siteId);
*/
}
return notifications;
} else {
return Promise.reject(null);
}
});
});
}
/**
* Get read notifications from site.
*
* @param {number} limitFrom Position of the first notification to get.
* @param {number} limitNumber Number of notifications to get.
* @param {boolean} [toDisplay=true] True if notifications will be displayed to the user, either in view or in a notification.
* @param {boolean} [forceCache] True if it should return cached data. Has priority over ignoreCache.
* @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. If not defined, use current site.
* @return {Promise<any[]>} Promise resolved with notifications.
*/
getReadNotifications(limitFrom: number, limitNumber: number, toDisplay: boolean = true,
forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise<any[]> {
return this.getNotifications(true, limitFrom, limitNumber, toDisplay, forceCache, ignoreCache, siteId);
}
/**
* Get unread notifications from site.
*
* @param {number} limitFrom Position of the first notification to get.
* @param {number} limitNumber Number of notifications to get.
* @param {boolean} [toDisplay=true] True if notifications will be displayed to the user, either in view or in a notification.
* @param {boolean} [forceCache] True if it should return cached data. Has priority over ignoreCache.
* @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. If not defined, use current site.
* @return {Promise<any[]>} Promise resolved with notifications.
*/
getUnreadNotifications(limitFrom: number, limitNumber: number, toDisplay: boolean = true,
forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise<any[]> {
return this.getNotifications(false, limitFrom, limitNumber, toDisplay, forceCache, ignoreCache, siteId);
}
/**
* Get unread notifications count. Do not cache calls.
*
* @param {number} [userId] The user id who received the notification. If not defined, use current user.
* @param {string} [siteId] Site ID. If not defined, use current site.
* @return {Promise<number>} Promise resolved with the message notifications count.
*/
getUnreadNotificationsCount(userId?: number, siteId?: string): Promise<number> {
return this.sitesProvider.getSite(siteId).then((site) => {
// @since 3.2
if (site.wsAvailable('message_popup_get_unread_popup_notification_count')) {
userId = userId || site.getUserId();
const params = {
useridto: userId
};
const preSets = {
getFromCache: false,
emergencyCache: false,
saveToCache: false,
typeExpected: 'number'
};
return site.read('message_popup_get_unread_popup_notification_count', params, preSets).catch(() => {
// Return no messages if the call fails.
return 0;
});
}
// Fallback call.
const limit = AddonNotificationsProvider.LIST_LIMIT + 1;
return this.getUnreadNotifications(0, limit, false, false, false, siteId).then((unread) => {
// Add + sign if there are more than the limit reachable.
return (unread.length > AddonNotificationsProvider.LIST_LIMIT) ?
AddonNotificationsProvider.LIST_LIMIT + '+' : unread.length;
}).catch(() => {
// Return no messages if the call fails.
return 0;
});
});
}
/**
* Mark message notification as read.
*
* @param {number} notificationId ID of notification to mark as read
* @returns {Promise<any>} Resolved when done.
*/
markNotificationRead(notificationId: number): Promise<any> {
const params = {
messageid: notificationId,
timeread: this.timeUtils.timestamp()
};
return this.sitesProvider.getCurrentSite().write('core_message_mark_message_read', params);
}
/**
* Invalidate get notification preferences.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when data is invalidated.
*/
invalidateNotificationPreferences(siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getNotificationPreferencesCacheKey());
});
}
/**
* Invalidates notifications list WS calls.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the list is invalidated.
*/
invalidateNotificationsList(siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getNotificationsCacheKey());
});
}
/**
* Returns whether or not we can count unread notifications precisely.
*
* @return {boolean} True if enabled, false otherwise.
* @since 3.2
*/
isPreciseNotificationCountEnabled(): boolean {
return this.sitesProvider.wsAvailableInCurrentSite('message_popup_get_unread_popup_notification_count');
}
/**
* Returns whether or not the notification preferences are enabled for the current site.
*
* @return {boolean} True if enabled, false otherwise.
* @since 3.2
*/
isNotificationPreferencesEnabled(): boolean {
return this.sitesProvider.wsAvailableInCurrentSite('core_message_get_user_notification_preferences');
}
}

View File

@ -0,0 +1,57 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable } from '@angular/core';
import { AddonNotificationsProvider } from './notifications';
import { CoreAppProvider } from '@providers/app';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreSettingsHandler, CoreSettingsHandlerData } from '@core/settings/providers/delegate';
/**
* Notifications settings handler.
*/
@Injectable()
export class AddonNotificationsSettingsHandler implements CoreSettingsHandler {
name = 'AddonNotifications';
priority = 500;
constructor(private appProvider: CoreAppProvider, private localNotificationsProvider: CoreLocalNotificationsProvider,
private notificationsProvider: AddonNotificationsProvider) {
}
/**
* Check if the handler is enabled on a site level.
*
* @return {boolean | Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
// Preferences or notification sound setting available.
return (this.notificationsProvider.isNotificationPreferencesEnabled() ||
this.localNotificationsProvider.isAvailable() && !this.appProvider.isDesktop());
}
/**
* Returns the data needed to render the handler.
*
* @return {CoreSettingsHandlerData} Data needed to render the handler.
*/
getDisplayData(): CoreSettingsHandlerData {
return {
icon: 'notifications',
title: 'addon.notifications.notificationpreferences',
page: 'AddonNotificationsSettingsPage',
class: 'addon-notifications-settings-handler'
};
}
}

View File

@ -82,9 +82,12 @@ import { AddonModFolderModule } from '@addon/mod/folder/folder.module';
import { AddonModPageModule } from '@addon/mod/page/page.module';
import { AddonModUrlModule } from '@addon/mod/url/url.module';
import { AddonModSurveyModule } from '@addon/mod/survey/survey.module';
import { AddonMessageOutputModule } from '@addon/messageoutput/messageoutput.module';
import { AddonMessageOutputAirnotifierModule } from '@addon/messageoutput/airnotifier/airnotifier.module';
import { AddonMessagesModule } from '@addon/messages/messages.module';
import { AddonNotesModule } from '../addon/notes/notes.module';
import { AddonPushNotificationsModule } from '@addon/pushnotifications/pushnotifications.module';
import { AddonNotificationsModule } from '@addon/notifications/notifications.module';
import { AddonRemoteThemesModule } from '@addon/remotethemes/remotethemes.module';
import { AddonQbehaviourModule } from '@addon/qbehaviour/qbehaviour.module';
@ -170,8 +173,11 @@ export const CORE_PROVIDERS: any[] = [
AddonModPageModule,
AddonModUrlModule,
AddonModSurveyModule,
AddonMessageOutputModule,
AddonMessageOutputAirnotifierModule,
AddonMessagesModule,
AddonNotesModule,
AddonNotificationsModule,
AddonPushNotificationsModule,
AddonRemoteThemesModule,
AddonQbehaviourModule

View File

@ -1,5 +1,7 @@
{
"about": "About",
"disableall": "Disable notifications",
"disabled": "Disabled",
"general": "General",
"loggedin": "Online",
"loggedoff": "Offline",

View File

@ -1,6 +1,8 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'core.settings.settings' | translate}}</ion-title>
<ion-buttons end>
</ion-buttons>
</ion-navbar>
</ion-header>
<core-split-view>

View File

@ -0,0 +1,94 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreUtilsProvider } from '@providers/utils/utils';
/**
* Settings helper service.
*/
@Injectable()
export class CoreSettingsHelper {
protected logger;
constructor(loggerProvider: CoreLoggerProvider, private utils: CoreUtilsProvider) {
this.logger = loggerProvider.getInstance('CoreSettingsHelper');
}
/**
* Get a certain processor from a list of processors.
*
* @param {any[]} processors List of processors.
* @param {string} name Name of the processor to get.
* @param {boolean} [fallback=true] True to return first processor if not found, false to not return any. Defaults to true.
* @return {any} Processor.
*/
getProcessor(processors: any[], name: string, fallback: boolean = true): any {
if (!processors || !processors.length) {
return;
}
for (let i = 0; i < processors.length; i++) {
if (processors[i].name == name) {
return processors[i];
}
}
// Processor not found, return first if requested.
if (fallback) {
return processors[0];
}
}
/**
* Return the components and notifications that have a certain processor.
*
* @param {string} processor Name of the processor to filter.
* @param {any[]} components Array of components.
* @return {any[]} Filtered components.
*/
getProcessorComponents(processor: string, components: any[]): any[] {
const result = [];
components.forEach((component) => {
// Create a copy of the component with an empty list of notifications.
const componentCopy = this.utils.clone(component);
componentCopy.notifications = [];
component.notifications.forEach((notification) => {
let hasProcessor = false;
for (let i = 0; i < notification.processors.length; i++) {
const proc = notification.processors[i];
if (proc.name == processor) {
hasProcessor = true;
notification.currentProcessor = proc;
break;
}
}
if (hasProcessor) {
// Add the notification.
componentCopy.notifications.push(notification);
}
});
if (componentCopy.notifications.length) {
// At least 1 notification added, add the component to the result.
result.push(componentCopy);
}
});
return result;
}
}

View File

@ -14,6 +14,7 @@
import { NgModule } from '@angular/core';
import { CoreSettingsDelegate } from './providers/delegate';
import { CoreSettingsHelper } from './providers/helper';
@NgModule({
declarations: [
@ -21,7 +22,8 @@ import { CoreSettingsDelegate } from './providers/delegate';
imports: [
],
providers: [
CoreSettingsDelegate
CoreSettingsDelegate,
CoreSettingsHelper
]
})
export class CoreSettingsModule {}