Merge pull request #1288 from albertgasset/MOBILE-2328

Mobile 2328
main
Juan Leyva 2018-04-09 16:15:53 +01:00 committed by GitHub
commit b8e887e368
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 2520 additions and 19 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,137 @@
// (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 {
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 The device object.
* @param {boolean} enable True to enable the device, false to disable it.
*/
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

@ -24,6 +24,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { AddonPushNotificationsProvider } from '@addon/pushnotifications/providers/pushnotifications'; import { AddonPushNotificationsProvider } from '@addon/pushnotifications/providers/pushnotifications';
import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate'; import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate';
import { CoreEmulatorHelperProvider } from '@core/emulator/providers/helper';
/** /**
* Handler to inject an option into main menu. * Handler to inject an option into main menu.
@ -46,7 +47,7 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr
private eventsProvider: CoreEventsProvider, private appProvider: CoreAppProvider, private eventsProvider: CoreEventsProvider, private appProvider: CoreAppProvider,
private localNotificationsProvider: CoreLocalNotificationsProvider, private textUtils: CoreTextUtilsProvider, private localNotificationsProvider: CoreLocalNotificationsProvider, private textUtils: CoreTextUtilsProvider,
private pushNotificationsProvider: AddonPushNotificationsProvider, utils: CoreUtilsProvider, private pushNotificationsProvider: AddonPushNotificationsProvider, utils: CoreUtilsProvider,
pushNotificationsDelegate: AddonPushNotificationsDelegate) { pushNotificationsDelegate: AddonPushNotificationsDelegate, private emulatorHelper: CoreEmulatorHelperProvider) {
eventsProvider.on(AddonMessagesProvider.READ_CHANGED_EVENT, (data) => { eventsProvider.on(AddonMessagesProvider.READ_CHANGED_EVENT, (data) => {
this.updateBadge(data.siteId); this.updateBadge(data.siteId);
@ -132,9 +133,9 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr
} }
if (this.appProvider.isDesktop() && this.localNotificationsProvider.isAvailable()) { if (this.appProvider.isDesktop() && this.localNotificationsProvider.isAvailable()) {
// @todo this.emulatorHelper.checkNewNotifications(
/*$mmEmulatorHelper.checkNewNotifications( AddonMessagesProvider.PUSH_SIMULATION_COMPONENT,
AddonMessagesProvider.PUSH_SIMULATION_COMPONENT, this.fetchMessages, this.getTitleAndText, siteId);*/ this.fetchMessages.bind(this), this.getTitleAndText.bind(this), siteId);
} }
return Promise.resolve(); return Promise.resolve();

View File

@ -20,6 +20,7 @@ import { CoreUserProvider } from '@core/user/providers/user';
import { AddonMessagesOfflineProvider } from './messages-offline'; import { AddonMessagesOfflineProvider } from './messages-offline';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreEmulatorHelperProvider } from '@core/emulator/providers/helper';
/** /**
* Service to handle messages. * Service to handle messages.
@ -40,7 +41,8 @@ export class AddonMessagesProvider {
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider,
private userProvider: CoreUserProvider, private messagesOffline: AddonMessagesOfflineProvider, private userProvider: CoreUserProvider, private messagesOffline: AddonMessagesOfflineProvider,
private utils: CoreUtilsProvider, private timeUtils: CoreTimeUtilsProvider) { private utils: CoreUtilsProvider, private timeUtils: CoreTimeUtilsProvider,
private emulatorHelper: CoreEmulatorHelperProvider) {
this.logger = logger.getInstance('AddonMessagesProvider'); this.logger = logger.getInstance('AddonMessagesProvider');
} }
@ -1088,7 +1090,6 @@ export class AddonMessagesProvider {
/** /**
* Store the last received message if it's newer than the last stored. * Store the last received message if it's newer than the last stored.
* @todo
* *
* @param {number} userIdFrom ID of the useridfrom retrieved, 0 for all users. * @param {number} userIdFrom ID of the useridfrom retrieved, 0 for all users.
* @param {any} message Last message received. * @param {any} message Last message received.
@ -1096,10 +1097,10 @@ export class AddonMessagesProvider {
* @return {Promise<any>} Promise resolved when done. * @return {Promise<any>} Promise resolved when done.
*/ */
protected storeLastReceivedMessageIfNeeded(userIdFrom: number, message: any, siteId?: string): Promise<any> { protected storeLastReceivedMessageIfNeeded(userIdFrom: number, message: any, siteId?: string): Promise<any> {
/*let component = mmaMessagesPushSimulationComponent; const component = AddonMessagesProvider.PUSH_SIMULATION_COMPONENT;
// Get the last received message. // Get the last received message.
return $mmEmulatorHelper.getLastReceivedNotification(component, siteId).then((lastMessage) => { return this.emulatorHelper.getLastReceivedNotification(component, siteId).then((lastMessage) => {
if (userIdFrom > 0 && (!message || !lastMessage)) { if (userIdFrom > 0 && (!message || !lastMessage)) {
// Seeing a single discussion. No received message or cannot know if it really is the last received message. Stop. // Seeing a single discussion. No received message or cannot know if it really is the last received message. Stop.
return; return;
@ -1110,9 +1111,8 @@ export class AddonMessagesProvider {
return; return;
} }
return $mmEmulatorHelper.storeLastReceivedNotification(component, message, siteId); return this.emulatorHelper.storeLastReceivedNotification(component, message, siteId);
});*/ });
return Promise.resolve();
} }
/** /**

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,36 @@
// (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
],
})
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="primary"></ion-icon></div>
<p>{{notification.timecreated | coreDateDayOrTime}}</p>
</ion-item>
<ion-item text-wrap>
<p><core-format-text [text]="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,194 @@
// (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 Whether we're refreshing data.
* @return {Promise<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;
unread.forEach(this.formatText.bind(this));
/* 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) => {
read.forEach(this.formatText.bind(this));
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 Array of notification objects.
*/
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 {any} notification The notification object.
*/
protected formatText(notification: any): void {
const text = notification.mobiletext.replace(/-{4,}/ig, '');
notification.mobiletext = this.textUtils.replaceNewLines(text, '<br>');
}
/**
* 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,10 @@
page-addon-notifications-settings {
.list-header {
margin-bottom: 0;
border-top: 0;
}
.toggle {
display: inline-block;
}
}

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 Processor object.
*/
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 True to enable the notification sound, false to disable it.
*/
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,112 @@
// (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 { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreEmulatorHelperProvider } from '@core/emulator/providers/helper';
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, private textUtils: CoreTextUtilsProvider,
private emulatorHelper: CoreEmulatorHelperProvider) {}
/**
* 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()) {
this.emulatorHelper.checkNewNotifications(
AddonNotificationsProvider.PUSH_SIMULATION_COMPONENT,
this.fetchNotifications.bind(this), this.getTitleAndText.bind(this), siteId);
}
return Promise.resolve(null);
}
/**
* Get the latest unread notifications from a site.
*
* @param {string} siteId Site ID.
* @return {Promise<any[]>} Promise resolved with the notifications.
*/
protected fetchNotifications(siteId: string): Promise<any[]> {
return this.notificationsProvider.getUnreadNotifications(0, undefined, true, false, true, siteId);
}
/**
* Given a notification, return the title and the text for the notification.
*
* @param {any} notification Notification.
* @return {Promise<any>} Promise resvoled with an object with title and text.
*/
protected getTitleAndText(notification: any): Promise<any> {
const data = {
title: notification.userfromfullname,
text: notification.mobiletext.replace(/-{4,}/ig, '')
};
data.text = this.textUtils.replaceNewLines(data.text, '<br>');
return Promise.resolve(data);
}
}

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';
import { CoreEmulatorHelperProvider } from '@core/emulator/providers/helper';
/**
* 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 = 'AddonNotificationsPushSimulation';
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,
private emulatorHelper: CoreEmulatorHelperProvider) {
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) {
// Store the last received notification. Don't block the user for this.
this.emulatorHelper.storeLastReceivedNotification(
AddonNotificationsProvider.PUSH_SIMULATION_COMPONENT, 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

@ -25,6 +25,7 @@ import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreConfigProvider } from '@providers/config'; import { CoreConfigProvider } from '@providers/config';
import { CoreConstants } from '@core/constants';
import { CoreConfigConstants } from '../../../configconstants'; import { CoreConfigConstants } from '../../../configconstants';
/** /**
@ -89,8 +90,7 @@ export class AddonPushNotificationsProvider {
* @return {Promise<PushOptions>} Promise with the push options resolved when done. * @return {Promise<PushOptions>} Promise with the push options resolved when done.
*/ */
protected getOptions(): Promise<PushOptions> { protected getOptions(): Promise<PushOptions> {
// @todo: CoreSettingsProvider.NOTIFICATION_SOUND return this.configProvider.get(CoreConstants.SETTINGS_NOTIFICATION_SOUND, true).then((soundEnabled) => {
return this.configProvider.get('CoreSettingsProvider.NOTIFICATION_SOUND', true).then((soundEnabled) => {
return { return {
android: { android: {
senderID: CoreConfigConstants.gcmpn, senderID: CoreConfigConstants.gcmpn,

View File

@ -82,9 +82,12 @@ import { AddonModFolderModule } from '@addon/mod/folder/folder.module';
import { AddonModPageModule } from '@addon/mod/page/page.module'; import { AddonModPageModule } from '@addon/mod/page/page.module';
import { AddonModUrlModule } from '@addon/mod/url/url.module'; import { AddonModUrlModule } from '@addon/mod/url/url.module';
import { AddonModSurveyModule } from '@addon/mod/survey/survey.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 { AddonMessagesModule } from '@addon/messages/messages.module';
import { AddonNotesModule } from '../addon/notes/notes.module'; import { AddonNotesModule } from '../addon/notes/notes.module';
import { AddonPushNotificationsModule } from '@addon/pushnotifications/pushnotifications.module'; import { AddonPushNotificationsModule } from '@addon/pushnotifications/pushnotifications.module';
import { AddonNotificationsModule } from '@addon/notifications/notifications.module';
import { AddonRemoteThemesModule } from '@addon/remotethemes/remotethemes.module'; import { AddonRemoteThemesModule } from '@addon/remotethemes/remotethemes.module';
import { AddonQbehaviourModule } from '@addon/qbehaviour/qbehaviour.module'; import { AddonQbehaviourModule } from '@addon/qbehaviour/qbehaviour.module';
import { AddonQtypeModule } from '@addon/qtype/qtype.module'; import { AddonQtypeModule } from '@addon/qtype/qtype.module';
@ -171,8 +174,11 @@ export const CORE_PROVIDERS: any[] = [
AddonModPageModule, AddonModPageModule,
AddonModUrlModule, AddonModUrlModule,
AddonModSurveyModule, AddonModSurveyModule,
AddonMessageOutputModule,
AddonMessageOutputAirnotifierModule,
AddonMessagesModule, AddonMessagesModule,
AddonNotesModule, AddonNotesModule,
AddonNotificationsModule,
AddonPushNotificationsModule, AddonPushNotificationsModule,
AddonRemoteThemesModule, AddonRemoteThemesModule,
AddonQbehaviourModule, AddonQbehaviourModule,

View File

@ -35,6 +35,7 @@ import { SQLite } from '@ionic-native/sqlite';
import { Zip } from '@ionic-native/zip'; import { Zip } from '@ionic-native/zip';
// Services that Mock Ionic Native in browser an desktop. // Services that Mock Ionic Native in browser an desktop.
import { BadgeMock } from './providers/badge';
import { CameraMock } from './providers/camera'; import { CameraMock } from './providers/camera';
import { ClipboardMock } from './providers/clipboard'; import { ClipboardMock } from './providers/clipboard';
import { FileMock } from './providers/file'; import { FileMock } from './providers/file';
@ -44,6 +45,7 @@ import { InAppBrowserMock } from './providers/inappbrowser';
import { LocalNotificationsMock } from './providers/local-notifications'; import { LocalNotificationsMock } from './providers/local-notifications';
import { MediaCaptureMock } from './providers/media-capture'; import { MediaCaptureMock } from './providers/media-capture';
import { NetworkMock } from './providers/network'; import { NetworkMock } from './providers/network';
import { PushMock } from './providers/push';
import { ZipMock } from './providers/zip'; import { ZipMock } from './providers/zip';
import { CoreEmulatorHelperProvider } from './providers/helper'; import { CoreEmulatorHelperProvider } from './providers/helper';
@ -89,7 +91,14 @@ export const IONIC_NATIVE_PROVIDERS = [
imports: [ imports: [
], ],
providers: [ providers: [
Badge, // @todo: Mock {
provide: Badge,
deps: [CoreAppProvider],
useFactory: (appProvider: CoreAppProvider): Badge => {
// Use platform instead of CoreAppProvider to prevent circular dependencies.
return appProvider.isMobile() ? new Badge() : new BadgeMock(appProvider);
}
},
CoreEmulatorHelperProvider, CoreEmulatorHelperProvider,
CoreEmulatorCaptureHelperProvider, CoreEmulatorCaptureHelperProvider,
{ {
@ -162,7 +171,14 @@ export const IONIC_NATIVE_PROVIDERS = [
return platform.is('cordova') ? new Network() : new NetworkMock(); return platform.is('cordova') ? new Network() : new NetworkMock();
} }
}, },
Push, // @todo: Mock {
provide: Push,
deps: [CoreAppProvider],
useFactory: (appProvider: CoreAppProvider): Push => {
// Use platform instead of CoreAppProvider to prevent circular dependencies.
return appProvider.isMobile() ? new Push() : new PushMock(appProvider);
}
},
SplashScreen, SplashScreen,
StatusBar, StatusBar,
SQLite, SQLite,

View File

@ -0,0 +1,123 @@
// (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 { Badge } from '@ionic-native/badge';
import { CoreAppProvider } from '@providers/app';
/**
* Emulates the Cordova Push plugin in desktop apps and in browser.
*/
@Injectable()
export class BadgeMock implements Badge {
constructor(private appProvider: CoreAppProvider) {}
/**
* Clear the badge of the app icon.
*
* @returns {Promise<boolean>}
*/
clear(): Promise<boolean> {
return Promise.reject('clear is only supported in mobile devices');
}
/**
* Set the badge of the app icon.
* @param {number} badgeNumber The new badge number.
* @returns {Promise<any>}
*/
set(badgeNumber: number): Promise<any> {
if (!this.appProvider.isDesktop()) {
return Promise.reject('set is not supported in browser');
}
try {
const app = require('electron').remote.app;
if (app.setBadgeCount(badgeNumber)) {
return Promise.resolve();
} else {
return Promise.reject(null);
}
} catch (ex) {
return Promise.reject(ex);
}
}
/**
* Get the badge of the app icon.
*
* @returns {Promise<any>}
*/
get(): Promise<any> {
if (!this.appProvider.isDesktop()) {
return Promise.reject('get is not supported in browser');
}
try {
const app = require('electron').remote.app;
return Promise.resolve(app.getBadgeCount());
} catch (ex) {
return Promise.reject(ex);
}
}
/**
* Increase the badge number.
*
* @param {number} increaseBy Count to add to the current badge number
* @returns {Promise<any>}
*/
increase(increaseBy: number): Promise<any> {
return Promise.reject('increase is only supported in mobile devices');
}
/**
* Decrease the badge number.
*
* @param {number} decreaseBy Count to subtract from the current badge number
* @returns {Promise<any>}
*/
decrease(decreaseBy: number): Promise<any> {
return Promise.reject('decrease is only supported in mobile devices');
}
/**
* Check support to show badges.
*
* @returns {Promise<any>}
*/
isSupported(): Promise<any> {
return Promise.reject('isSupported is only supported in mobile devices');
}
/**
* Determine if the app has permission to show badges.
*
* @returns {Promise<any>}
*/
hasPermission(): Promise<any> {
return Promise.reject('hasPermission is only supported in mobile devices');
}
/**
* Register permission to set badge notifications
*
* @returns {Promise<any>}
*/
requestPermission(): Promise<any> {
return Promise.reject('requestPermission is only supported in mobile devices');
}
}

View File

@ -17,9 +17,15 @@ import { CoreFileProvider } from '@providers/file';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { File } from '@ionic-native/file'; import { File } from '@ionic-native/file';
import { LocalNotifications } from '@ionic-native/local-notifications'; import { LocalNotifications } from '@ionic-native/local-notifications';
import { CoreAppProvider } from '@providers/app';
import { CoreInitDelegate, CoreInitHandler } from '@providers/init'; import { CoreInitDelegate, CoreInitHandler } from '@providers/init';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { FileTransferErrorMock } from './file-transfer'; import { FileTransferErrorMock } from './file-transfer';
import { CoreEmulatorCaptureHelperProvider } from './capture-helper'; import { CoreEmulatorCaptureHelperProvider } from './capture-helper';
import { CoreConstants } from '../../constants';
/** /**
* Helper service for the emulator feature. It also acts as an init handler. * Helper service for the emulator feature. It also acts as an init handler.
@ -30,9 +36,38 @@ export class CoreEmulatorHelperProvider implements CoreInitHandler {
priority = CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 500; priority = CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 500;
blocking = true; blocking = true;
protected logger;
// Variables for database.
protected LAST_RECEIVED_NOTIFICATION_TABLE = 'core_emulator_last_received_notification';
protected tablesSchema = [
{
name: this.LAST_RECEIVED_NOTIFICATION_TABLE,
columns: [
{
name: 'component',
type: 'TEXT'
},
{
name: 'id',
type: 'INTEGER',
},
{
name: 'timecreated',
type: 'INTEGER',
},
],
primaryKeys: ['component']
}
];
constructor(private file: File, private fileProvider: CoreFileProvider, private utils: CoreUtilsProvider, constructor(private file: File, private fileProvider: CoreFileProvider, private utils: CoreUtilsProvider,
initDelegate: CoreInitDelegate, private localNotif: LocalNotifications, logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private localNotif: LocalNotifications,
private captureHelper: CoreEmulatorCaptureHelperProvider) { } private captureHelper: CoreEmulatorCaptureHelperProvider, private timeUtils: CoreTimeUtilsProvider,
private appProvider: CoreAppProvider, private localNotifProvider: CoreLocalNotificationsProvider) {
this.logger = logger.getInstance('CoreEmulatorHelper');
sitesProvider.createTablesFromSchema(this.tablesSchema);
}
/** /**
* Load the Mocks that need it. * Load the Mocks that need it.
@ -52,4 +87,130 @@ export class CoreEmulatorHelperProvider implements CoreInitHandler {
return this.utils.allPromises(promises); return this.utils.allPromises(promises);
} }
/**
* Check if there are new notifications, triggering a local notification if found.
* Only for desktop apps since they don't support push notifications.
*
* @param {string} component Component to check.
* @param {Function} fetchFn Function that receives a site ID and returns a Promise resolved with an array of notifications.
* @param {Function} getDataFn Function that receives a notification and returns a promise resolved with the title and text.
* @param {string} [siteId] Site ID to check. If not defined, check all sites.
* @return {Promise<any>} Promise resolved when done.
*/
checkNewNotifications(component: string, fetchFn: Function, getDataFn: Function, siteId?: string): Promise<any> {
if (!this.appProvider.isDesktop() || !this.localNotifProvider.isAvailable()) {
return Promise.resolve(null);
}
if (!this.appProvider.isOnline()) {
this.logger.debug('Cannot check push notifications because device is offline.');
return Promise.reject(null);
}
let promise: Promise<string[]>;
if (!siteId) {
// No site ID defined, check all sites.
promise = this.sitesProvider.getSitesIds();
} else {
promise = Promise.resolve([siteId]);
}
return promise.then((siteIds) => {
const sitePromises = siteIds.map((siteId) => {
// Check new notifications for each site.
return this.checkNewNotificationsForSite(component, fetchFn, getDataFn, siteId);
});
return Promise.all(sitePromises);
});
}
/**
* Check if there are new notifications for a certain site, triggering a local notification if found.
*
* @param {string} component Component to check.
* @param {Function} fetchFn Function that receives a site ID and returns a Promise resolved with an array of notifications.
* @param {Function} getDataFn Function that receives a notification and returns a promise resolved with the title and text.
* @param {string} siteId Site ID to check.
* @return {Promise<any>} Promise resolved when done.
*/
protected checkNewNotificationsForSite(component: string, fetchFn: Function, getDataFn: Function, siteId: string)
: Promise<any> {
// Get the last received notification in the app.
return this.getLastReceivedNotification(component, siteId).then((lastNotification) => {
// Now fetch the latest notifications from the server.
return fetchFn(siteId).then((notifications) => {
if (!lastNotification || !notifications.length) {
// No last notification stored (first call) or no new notifications. Stop.
return;
}
const notification = notifications[0];
if (notification.id == lastNotification.id || notification.timecreated <= lastNotification.timecreated ||
this.timeUtils.timestamp() - notification.timecreated > CoreConstants.SECONDS_DAY) {
// There are no new notifications or the newest one happened more than a day ago, stop.
return;
}
// There is a new notification, show it.
return getDataFn(notification).then((titleAndText) => {
const localNotif = {
id: 1,
at: new Date(),
title: titleAndText.title,
text: titleAndText.text,
data: {
notif: notification,
site: siteId
}
};
return this.localNotifProvider.schedule(localNotif, component, siteId);
});
});
});
}
/**
* Get the last notification received in a certain site for a certain component.
*
* @param {string} component Component of the notification to get.
* @param {string} siteId Site ID of the notification.
* @return {Promise<any>} Promise resolved with the notification or false if not found.
*/
getLastReceivedNotification(component: string, siteId: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecord(this.LAST_RECEIVED_NOTIFICATION_TABLE, {component: component});
}).catch(() => {
return false;
});
}
/**
* Store the last notification received in a certain site.
*
* @param {string} component Component of the notification to store.
* @param {any} notification Notification to store.
* @param {string} siteId Site ID of the notification.
* @return {Promise<any>} Promise resolved when done.
*/
storeLastReceivedNotification(component: string, notification: any, siteId: string): Promise<any> {
if (!notification) {
// No notification, store a fake one.
notification = {id: -1, timecreated: 0};
}
return this.sitesProvider.getSite(siteId).then((site) => {
const entry = {
component: component,
id: notification.id,
timecreated: notification.timecreated,
};
return site.getDb().insertRecord(this.LAST_RECEIVED_NOTIFICATION_TABLE, entry);
});
}
} }

View File

@ -0,0 +1,184 @@
// (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 { Observable } from 'rxjs/Observable';
import { Channel, EventResponse, Push, PushEvent, PushObject, PushOptions } from '@ionic-native/push';
import { CoreAppProvider } from '@providers/app';
/**
* Emulates the Cordova Push plugin in desktop apps and in browser.
*/
@Injectable()
export class PushMock implements Push {
constructor(private appProvider: CoreAppProvider) {
}
/**
* Init push notifications
*
* @param {PushOptions} options
* @return {PushObject}
*/
init(options: PushOptions): PushObject {
return new PushObjectMock(this.appProvider);
}
/**
* Check whether the push notification permission has been granted.
*
* @return {Promise<{isEnabled: boolean}>} Returns a Promise that resolves with an object with one property: isEnabled, a
* boolean that indicates if permission has been granted.
*/
hasPermission(): Promise<{isEnabled: boolean}> {
return Promise.reject('hasPermission is only supported in mobile devices');
}
/**
* Create a new notification channel for Android O and above.
*
* @param {Channel} channel
*/
createChannel(channel?: Channel): Promise<any> {
return Promise.reject('createChannel is only supported in mobile devices');
}
/**
* Delete a notification channel for Android O and above.
*
* @param {string} id
*/
deleteChannel(id?: string): Promise<any> {
return Promise.reject('deleteChannel is only supported in mobile devices');
}
/**
* Returns a list of currently configured channels.
*
* @return {Promise<Channel[]>}
*/
listChannels(): Promise<Channel[]> {
return Promise.reject('listChannels is only supported in mobile devices');
}
}
/**
* Emulates the PushObject class in desktop apps and in browser.
*/
export class PushObjectMock extends PushObject {
constructor(private appProvider: CoreAppProvider) {
super({});
}
/**
* Adds an event listener
* @param event {string}
* @return {Observable<EventResponse>}
*/
on(event: PushEvent): Observable<EventResponse> {
return Observable.empty();
}
/**
* The unregister method is used when the application no longer wants to receive push notifications.
* Beware that this cleans up all event handlers previously registered,
* so you will need to re-register them if you want them to function again without an application reload.
*/
unregister(): Promise<any> {
return Promise.reject('unregister is only supported in mobile devices');
}
/**
* Set the badge count visible when the app is not running
*
* The count is an integer indicating what number should show up in the badge.
* Passing 0 will clear the badge.
* Each notification event contains a data.count value which can be used to set the badge to correct number.
*
* @param count
*/
setApplicationIconBadgeNumber(count?: number): Promise<any> {
if (!this.appProvider.isDesktop()) {
return Promise.reject('setApplicationIconBadgeNumber is not supported in browser');
}
try {
const app = require('electron').remote.app;
if (app.setBadgeCount(count)) {
return Promise.resolve();
} else {
return Promise.reject(null);
}
} catch (ex) {
return Promise.reject(ex);
}
}
/**
* Get the current badge count visible when the app is not running
* successHandler gets called with an integer which is the current badge count
*/
getApplicationIconBadgeNumber(): Promise<number> {
if (!this.appProvider.isDesktop()) {
return Promise.reject('getApplicationIconBadgeNumber is not supported in browser');
}
try {
const app = require('electron').remote.app;
return Promise.resolve(app.getBadgeCount());
} catch (ex) {
return Promise.reject(ex);
}
}
/**
* iOS only
* Tells the OS that you are done processing a background push notification.
* successHandler gets called when background push processing is successfully completed.
* @param [id]
*/
finish(id?: string): Promise<any> {
return Promise.reject('finish is only supported in mobile devices');
}
/**
* Tells the OS to clear all notifications from the Notification Center
*/
clearAllNotifications(): Promise<any> {
return Promise.reject('clearAllNotifications is only supported in mobile devices');
}
/**
* The subscribe method is used when the application wants to subscribe a new topic to receive push notifications.
* @param topic {string} Topic to subscribe to.
* @return {Promise<any>}
*/
subscribe(topic: string): Promise<any> {
return Promise.reject('subscribe is only supported in mobile devices');
}
/**
* The unsubscribe method is used when the application no longer wants to receive push notifications from a specific topic but
* continue to receive other push messages.
*
* @param topic {string} Topic to unsubscribe from.
* @return {Promise<any>}
*/
unsubscribe(topic: string): Promise<any> {
return Promise.reject('unsubscribe is only supported in mobile devices');
}
}

View File

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

View File

@ -1,6 +1,8 @@
<ion-header> <ion-header>
<ion-navbar> <ion-navbar>
<ion-title>{{ 'core.settings.settings' | translate}}</ion-title> <ion-title>{{ 'core.settings.settings' | translate}}</ion-title>
<ion-buttons end>
</ion-buttons>
</ion-navbar> </ion-navbar>
</ion-header> </ion-header>
<core-split-view> <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 { NgModule } from '@angular/core';
import { CoreSettingsDelegate } from './providers/delegate'; import { CoreSettingsDelegate } from './providers/delegate';
import { CoreSettingsHelper } from './providers/helper';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -21,7 +22,8 @@ import { CoreSettingsDelegate } from './providers/delegate';
imports: [ imports: [
], ],
providers: [ providers: [
CoreSettingsDelegate CoreSettingsDelegate,
CoreSettingsHelper
] ]
}) })
export class CoreSettingsModule {} export class CoreSettingsModule {}