From b9c0bb56533e23e76965380871729d48e8cdad1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 23 Feb 2018 14:44:17 +0100 Subject: [PATCH] MOBILE-2327 pushnotifications: Add push notifications delegate --- src/addon/files/providers/files.ts | 2 +- src/addon/messages/messages.module.ts | 28 +++- .../messages/providers/mainmenu-handler.ts | 6 +- src/addon/messages/providers/messages.ts | 25 +++- .../pushnotifications/providers/delegate.ts | 131 ++++++++++++++++++ .../providers/pushnotifications.ts | 80 +++++++++++ .../pushnotifications.module.ts | 31 +++++ src/app/app.component.ts | 8 +- src/app/app.module.ts | 4 +- .../providers/module-prefetch-delegate.ts | 2 +- src/core/courses/providers/courses.ts | 2 +- src/core/emulator/emulator.module.ts | 6 + src/core/login/providers/helper.ts | 2 +- src/providers/sites.ts | 13 ++ 14 files changed, 325 insertions(+), 15 deletions(-) create mode 100644 src/addon/pushnotifications/providers/delegate.ts create mode 100644 src/addon/pushnotifications/providers/pushnotifications.ts create mode 100644 src/addon/pushnotifications/pushnotifications.module.ts diff --git a/src/addon/files/providers/files.ts b/src/addon/files/providers/files.ts index 268a835a3..5c45c913e 100644 --- a/src/addon/files/providers/files.ts +++ b/src/addon/files/providers/files.ts @@ -35,7 +35,7 @@ export class AddonFilesProvider { * @return {boolean} Whether the WS is available, false otherwise. */ canGetPrivateFilesInfo(): boolean { - return this.sitesProvider.getCurrentSite().wsAvailable('core_user_get_private_files_info'); + return this.sitesProvider.wsAvailableInCurrentSite('core_user_get_private_files_info'); } /** diff --git a/src/addon/messages/messages.module.ts b/src/addon/messages/messages.module.ts index 779d1c8f9..f1cd01549 100644 --- a/src/addon/messages/messages.module.ts +++ b/src/addon/messages/messages.module.ts @@ -27,6 +27,10 @@ import { AddonMessagesDiscussionLinkHandler } from './providers/discussion-link- import { AddonMessagesIndexLinkHandler } from './providers/index-link-handler'; import { AddonMessagesSyncCronHandler } from './providers/sync-cron-handler'; import { CoreEventsProvider } from '../../providers/events'; +import { CoreAppProvider } from '../../providers/app'; +import { CoreSitesProvider } from '../../providers/sites'; +import { CoreLocalNotificationsProvider } from '../../providers/local-notifications'; +import { CoreContentLinksHelperProvider } from '../../core/contentlinks/providers/helper'; @NgModule({ declarations: [ @@ -49,7 +53,9 @@ export class AddonMessagesModule { contentLinksDelegate: CoreContentLinksDelegate, indexLinkHandler: AddonMessagesIndexLinkHandler, discussionLinkHandler: AddonMessagesDiscussionLinkHandler, sendMessageHandler: AddonMessagesSendMessageUserHandler, userDelegate: CoreUserDelegate, cronDelegate: CoreCronDelegate, syncHandler: AddonMessagesSyncCronHandler, - network: Network, messagesSync: AddonMessagesSyncProvider) { + network: Network, messagesSync: AddonMessagesSyncProvider, appProvider: CoreAppProvider, + localNotifications: CoreLocalNotificationsProvider, messagesProvider: AddonMessagesProvider, + sitesProvider: CoreSitesProvider, linkHelper: CoreContentLinksHelperProvider) { // Register handlers. mainMenuDelegate.registerHandler(mainmenuHandler); contentLinksDelegate.registerHandler(indexLinkHandler); @@ -62,5 +68,25 @@ export class AddonMessagesModule { network.onConnect().subscribe(() => { messagesSync.syncAllDiscussions(undefined, true); }); + + const notificationClicked = (notification: any): void => { + messagesProvider.isMessagingEnabledForSite(notification.site).then(() => { + sitesProvider.isFeatureDisabled('$mmSideMenuDelegate_mmaMessages', notification.site).then((disabled) => { + if (disabled) { + // Messages are disabled, stop. + return; + } + + messagesProvider.invalidateDiscussionsCache().finally(() => { + linkHelper.goInSite(undefined, 'AddonMessagesIndexPage', undefined, notification.site); + }); + }); + }); + }; + + if (appProvider.isDesktop()) { + // Listen for clicks in simulated push notifications. + localNotifications.registerClick(AddonMessagesProvider.PUSH_SIMULATION_COMPONENT, notificationClicked); + } } } diff --git a/src/addon/messages/providers/mainmenu-handler.ts b/src/addon/messages/providers/mainmenu-handler.ts index d279301c7..dcee4cc5f 100644 --- a/src/addon/messages/providers/mainmenu-handler.ts +++ b/src/addon/messages/providers/mainmenu-handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Inject } from '@angular/core'; import { AddonMessagesProvider } from './messages'; import { CoreMainMenuDelegate, CoreMainMenuHandler, CoreMainMenuHandlerToDisplay } from '@core/mainmenu/providers/delegate'; import { CoreCronHandler } from '@providers/cron'; @@ -21,6 +21,7 @@ import { CoreEventsProvider } from '@providers/events'; import { CoreAppProvider } from '@providers/app'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { AddonPushNotificationsProvider } from '@addon/pushnotifications/providers/pushnotifications'; /** * Handler to inject an option into main menu. @@ -34,7 +35,8 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr constructor(private messagesProvider: AddonMessagesProvider, private sitesProvider: CoreSitesProvider, private eventsProvider: CoreEventsProvider, private appProvider: CoreAppProvider, - private localNotificationsProvider: CoreLocalNotificationsProvider, private textUtils: CoreTextUtilsProvider) { + private localNotificationsProvider: CoreLocalNotificationsProvider, private textUtils: CoreTextUtilsProvider, + private pushNotificationsProvider: AddonPushNotificationsProvider) { eventsProvider.on(AddonMessagesProvider.READ_CHANGED_EVENT, (data) => { this.updateBadge(data.siteId); diff --git a/src/addon/messages/providers/messages.ts b/src/addon/messages/providers/messages.ts index 1f7076827..28e10154a 100644 --- a/src/addon/messages/providers/messages.ts +++ b/src/addon/messages/providers/messages.ts @@ -33,6 +33,7 @@ export class AddonMessagesProvider { static READ_CRON_EVENT = 'read_cron_event'; static SPLIT_VIEW_LOAD_EVENT = 'split_view_load_event'; static POLL_INTERVAL = 10000; + static PUSH_SIMULATION_COMPONENT = 'AddonMessagesPushSimulation'; protected logger; @@ -521,7 +522,7 @@ export class AddonMessagesProvider { * @since 3.2 */ isMarkAllMessagesReadEnabled(): boolean { - return this.sitesProvider.getCurrentSite().wsAvailable('core_message_mark_all_messages_as_read'); + return this.sitesProvider.wsAvailableInCurrentSite('core_message_mark_all_messages_as_read'); } /** @@ -531,7 +532,25 @@ export class AddonMessagesProvider { * @since 3.2 */ isMessageCountEnabled(): boolean { - return this.sitesProvider.getCurrentSite().wsAvailable('core_message_get_unread_conversations_count'); + return this.sitesProvider.wsAvailableInCurrentSite('core_message_get_unread_conversations_count'); + } + + /** + * Returns whether or not messaging is enabled for a certain site. + * + * This could call a WS so do not abuse this method. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when enabled, otherwise rejected. + */ + isMessagingEnabledForSite(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + if (!site.canUseAdvancedFeature('messaging')) { + return Promise.reject(null); + } + + return Promise.resolve(true); + }); } /** @@ -553,7 +572,7 @@ export class AddonMessagesProvider { * @since 3.2 */ isSearchMessagesEnabled(): boolean { - return this.sitesProvider.getCurrentSite().wsAvailable('core_message_data_for_messagearea_search_messages'); + return this.sitesProvider.wsAvailableInCurrentSite('core_message_data_for_messagearea_search_messages'); } /** diff --git a/src/addon/pushnotifications/providers/delegate.ts b/src/addon/pushnotifications/providers/delegate.ts new file mode 100644 index 000000000..14b520240 --- /dev/null +++ b/src/addon/pushnotifications/providers/delegate.ts @@ -0,0 +1,131 @@ +// (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'; + +/** + * Service to handle push notifications clicks. + */ +@Injectable() +export class AddonPushNotificationsDelegate { + + protected logger; + protected clickHandlers: { [s: string]: Function } = {}; + protected receiveHandlers: { [s: string]: Function } = {}; + protected counterHandlers: { [s: string]: string } = {}; + + constructor(loggerProvider: CoreLoggerProvider) { + this.logger = loggerProvider.getInstance('AddonPushNotificationsDelegate'); + } + + /** + * Function called when a push notification is clicked. Sends notification to handlers. + * + * @param {any} notification Notification clicked. + */ + clicked(notification: any): void { + for (const name in this.clickHandlers) { + const callback = this.clickHandlers[name]; + if (typeof callback == 'function') { + const treated = callback(notification); + if (treated) { + return; // Stop execution when notification is treated. + } + } + } + } + + /** + * Function called when a push notification is received in foreground (cannot tell when it's received in background). + * Sends notification to all handlers. + * + * @param {any} notification Notification received. + */ + received(notification: any): void { + for (const name in this.receiveHandlers) { + const callback = this.receiveHandlers[name]; + if (typeof callback == 'function') { + callback(notification); + } + } + } + + /** + * Register a push notifications handler for CLICKS. + * When a notification is clicked, the handler will receive a notification to treat. + * + * @param {string} name Handler's name. + * @param {Function} callback The callback function. Will get as parameter the clicked notification. + * @description + * The handler should return true if the notification is the one expected, false otherwise. + * @see {@link AddonPushNotificationsDelegate#clicked} + */ + registerHandler(name: string, callback: Function): void { + this.logger.debug(`Registered handler '${name}' as CLICK push notification handler.`); + this.clickHandlers[name] = callback; + } + + /** + * Register a push notifications handler for RECEIVE notifications in foreground (cannot tell when it's received in background). + * When a notification is received, the handler will receive a notification to treat. + * + * @param {string} name Handler's name. + * @param {Function} callback The callback function. Will get as parameter the clicked notification. + * @see {@link AddonPushNotificationsDelegate#received} + */ + registerReceiveHandler(name: string, callback: Function): void { + this.logger.debug(`Registered handler '${name}' as RECEIVE push notification handler.`); + this.receiveHandlers[name] = callback; + } + + /** + * Unregister a push notifications handler for RECEIVE notifications. + * + * @param {string} name Handler's name. + */ + unregisterReceiveHandler(name: string): void { + this.logger.debug(`Unregister handler '${name}' from RECEIVE push notification handlers.`); + delete this.receiveHandlers[name]; + } + + /** + * Register a push notifications handler for update badge counter. + * + * @param {string} name Handler's name. + */ + registerCounterHandler(name: string): void { + this.logger.debug(`Registered handler '${name}' as badge counter handler.`); + this.counterHandlers[name] = name; + } + + /** + * Check if a counter handler is present. + * + * @param {string} name Handler's name. + * @return {boolean} If handler name is present. + */ + isCounterHandlerRegistered(name: string): boolean { + return typeof this.counterHandlers[name] != 'undefined'; + } + + /** + * Get all counter badge handlers. + * + * @return {any} with all the handler names. + */ + getCounterHandlers(): any { + return this.counterHandlers; + } +} diff --git a/src/addon/pushnotifications/providers/pushnotifications.ts b/src/addon/pushnotifications/providers/pushnotifications.ts new file mode 100644 index 000000000..cfcad478f --- /dev/null +++ b/src/addon/pushnotifications/providers/pushnotifications.ts @@ -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 { Injectable } from '@angular/core'; +import { Platform } from 'ionic-angular'; +import { CoreAppProvider } from '@providers/app'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { AddonPushNotificationsDelegate } from './delegate'; + +/** + * Service to handle push notifications. + */ +@Injectable() +export class AddonPushNotificationsProvider { + + protected logger; + protected pushID: string; + protected appDB: any; + + // Variables for database. + protected BADGE_TABLE = 'mma_pushnotifications_badge'; + protected tablesSchema = [ + { + name: this.BADGE_TABLE, + columns: [ + { + name: 'siteid', + type: 'INTEGER' + }, + { + name: 'addon', + type: 'TEXT' + }, + { + name: 'number', + type: 'INTEGER' + } + ] + } + ]; + + constructor(logger: CoreLoggerProvider, protected appProvider: CoreAppProvider, private platform: Platform, + protected pushNotificationsDelegate: AddonPushNotificationsDelegate, protected sitesProvider: CoreSitesProvider) { + this.logger = logger.getInstance('AddonPushNotificationsProvider'); + this.appDB = appProvider.getDB(); + this.appDB.createTablesFromSchema(this.tablesSchema); + } + + /** + * Get the pushID for this device. + * + * @return {string} Push ID. + */ + getPushId(): string { + return this.pushID; + } + + /** + * Function called when a push notification is clicked. Redirect the user to the right state. + * + * @param {any} notification Notification. + */ + notificationClicked(notification: any): void { + this.platform.ready().then(() => { + this.pushNotificationsDelegate.clicked(notification); + }); + } +} diff --git a/src/addon/pushnotifications/pushnotifications.module.ts b/src/addon/pushnotifications/pushnotifications.module.ts new file mode 100644 index 000000000..ba2118a1d --- /dev/null +++ b/src/addon/pushnotifications/pushnotifications.module.ts @@ -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 { AddonPushNotificationsProvider } from './providers/pushnotifications'; +import { AddonPushNotificationsDelegate } from './providers/delegate'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + providers: [ + AddonPushNotificationsProvider, + AddonPushNotificationsDelegate + ] +}) +export class AddonPushNotificationsModule { + constructor() {} +} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 4bdeb273d..e3ba3417f 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -16,10 +16,10 @@ import { Component, OnInit } from '@angular/core'; import { Platform } from 'ionic-angular'; import { StatusBar } from '@ionic-native/status-bar'; import { SplashScreen } from '@ionic-native/splash-screen'; -import { CoreAppProvider } from '../providers/app'; -import { CoreEventsProvider } from '../providers/events'; -import { CoreLoggerProvider } from '../providers/logger'; -import { CoreLoginHelperProvider } from '../core/login/providers/helper'; +import { CoreAppProvider } from '@providers/app'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreLoginHelperProvider } from '@core/login/providers/helper'; @Component({ templateUrl: 'app.html' diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b54aeed8e..c4f64116f 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -71,6 +71,7 @@ import { AddonFilesModule } from '@addon/files/files.module'; import { AddonModBookModule } from '@addon/mod/book/book.module'; import { AddonModLabelModule } from '@addon/mod/label/label.module'; import { AddonMessagesModule } from '@addon/messages/messages.module'; +import { AddonPushNotificationsModule } from '@addon/pushnotifications/pushnotifications.module'; // For translate loader. AoT requires an exported function for factories. export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { @@ -113,7 +114,8 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { AddonFilesModule, AddonModBookModule, AddonModLabelModule, - AddonMessagesModule + AddonMessagesModule, + AddonPushNotificationsModule ], bootstrap: [IonicApp], entryComponents: [ diff --git a/src/core/course/providers/module-prefetch-delegate.ts b/src/core/course/providers/module-prefetch-delegate.ts index a80e9c74f..964f67ac2 100644 --- a/src/core/course/providers/module-prefetch-delegate.ts +++ b/src/core/course/providers/module-prefetch-delegate.ts @@ -238,7 +238,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { * @return {boolean} True if can check updates, false otherwise. */ canCheckUpdates(): boolean { - return this.sitesProvider.getCurrentSite().wsAvailable('core_course_check_updates'); + return this.sitesProvider.wsAvailableInCurrentSite('core_course_check_updates'); } /** diff --git a/src/core/courses/providers/courses.ts b/src/core/courses/providers/courses.ts index f71deddfe..395c40838 100644 --- a/src/core/courses/providers/courses.ts +++ b/src/core/courses/providers/courses.ts @@ -355,7 +355,7 @@ export class CoreCoursesProvider { * @return {boolean} Whether get courses by field is available. */ isGetCoursesByFieldAvailable(): boolean { - return this.sitesProvider.getCurrentSite().wsAvailable('core_course_get_courses_by_field'); + return this.sitesProvider.wsAvailableInCurrentSite('core_course_get_courses_by_field'); } /** diff --git a/src/core/emulator/emulator.module.ts b/src/core/emulator/emulator.module.ts index 0437615a4..8069da54e 100644 --- a/src/core/emulator/emulator.module.ts +++ b/src/core/emulator/emulator.module.ts @@ -16,8 +16,10 @@ import { NgModule } from '@angular/core'; import { Platform } from 'ionic-angular'; // Ionic Native services. +import { Badge } from '@ionic-native/badge'; import { Camera } from '@ionic-native/camera'; import { Clipboard } from '@ionic-native/clipboard'; +import { Device } from '@ionic-native/device'; import { File } from '@ionic-native/file'; import { FileTransfer } from '@ionic-native/file-transfer'; import { Globalization } from '@ionic-native/globalization'; @@ -26,6 +28,7 @@ import { Keyboard } from '@ionic-native/keyboard'; import { LocalNotifications } from '@ionic-native/local-notifications'; import { MediaCapture } from '@ionic-native/media-capture'; import { Network } from '@ionic-native/network'; +import { Push } from '@ionic-native/push'; import { SplashScreen } from '@ionic-native/splash-screen'; import { StatusBar } from '@ionic-native/status-bar'; import { SQLite } from '@ionic-native/sqlite'; @@ -68,6 +71,7 @@ import { CoreInitDelegate } from '../../providers/init'; imports: [ ], providers: [ + Badge, CoreEmulatorHelperProvider, CoreEmulatorCaptureHelperProvider, { @@ -84,6 +88,7 @@ import { CoreInitDelegate } from '../../providers/init'; return appProvider.isMobile() ? new Clipboard() : new ClipboardMock(appProvider); } }, + Device, { provide: File, deps: [CoreAppProvider, CoreTextUtilsProvider], @@ -139,6 +144,7 @@ import { CoreInitDelegate } from '../../providers/init'; return platform.is('cordova') ? new Network() : new NetworkMock(); } }, + Push, SplashScreen, StatusBar, SQLite, diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index 6068586ad..a36be792d 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -901,7 +901,7 @@ export class CoreLoginHelperProvider { return; } - if (!this.sitesProvider.getCurrentSite().wsAvailable('core_user_agree_site_policy')) { + if (!this.sitesProvider.wsAvailableInCurrentSite('core_user_agree_site_policy')) { // WS not available, stop. return; } diff --git a/src/providers/sites.ts b/src/providers/sites.ts index bcefaba14..21aa9efd3 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -1212,4 +1212,17 @@ export class CoreSitesProvider { this.sites[id].getDb().createTablesFromSchema(tables); } } + + /** + * Check if a WS is available in the current site, if any. + * + * @param {string} method WS name. + * @param {boolean} [checkPrefix=true] When true also checks with the compatibility prefix. + * @return {boolean} Whether the WS is available. + */ + wsAvailableInCurrentSite(method: string, checkPrefix: boolean = true): boolean { + const site = this.getCurrentSite(); + + return site && site.wsAvailable(method, checkPrefix); + } }