diff --git a/src/addons/messageoutput/airnotifier/airnotifier.module.ts b/src/addons/messageoutput/airnotifier/airnotifier.module.ts new file mode 100644 index 000000000..2d8b19f1b --- /dev/null +++ b/src/addons/messageoutput/airnotifier/airnotifier.module.ts @@ -0,0 +1,49 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { Routes } from '@angular/router'; + +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { AddonMessageOutputDelegate } from '@addons/messageoutput/services/messageoutput-delegate'; +import { + AddonMessageOutputAirnotifierHandler, + AddonMessageOutputAirnotifierHandlerService, +} from './services/handlers/messageoutput'; + +const routes: Routes = [ + { + path: AddonMessageOutputAirnotifierHandlerService.PAGE_NAME, + loadChildren: () => import('./pages/devices/devices.module').then( m => m.AddonMessageOutputAirnotifierDevicesPageModule), + }, +]; + +@NgModule({ + declarations: [ + ], + imports: [ + CoreMainMenuTabRoutingModule.forChild(routes), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => () => { + AddonMessageOutputDelegate.instance.registerHandler(AddonMessageOutputAirnotifierHandler.instance); + }, + }, + ], +}) +export class AddonMessageOutputAirnotifierModule {} diff --git a/src/addons/messageoutput/airnotifier/lang.json b/src/addons/messageoutput/airnotifier/lang.json new file mode 100644 index 000000000..a6f460bbb --- /dev/null +++ b/src/addons/messageoutput/airnotifier/lang.json @@ -0,0 +1,3 @@ +{ + "processorsettingsdesc": "Configure devices" +} \ No newline at end of file diff --git a/src/addons/messageoutput/airnotifier/pages/devices/devices.html b/src/addons/messageoutput/airnotifier/pages/devices/devices.html new file mode 100644 index 000000000..134300b25 --- /dev/null +++ b/src/addons/messageoutput/airnotifier/pages/devices/devices.html @@ -0,0 +1,27 @@ + + + + + + {{ 'addon.messageoutput_airnotifier.processorsettingsdesc' | translate }} + + + + + + + + + + + {{ device.name }} {{ device.model }} {{ device.platform }} {{ device.version }} + ({{ 'core.currentdevice' | translate }}) + + + + + + + + diff --git a/src/addons/messageoutput/airnotifier/pages/devices/devices.module.ts b/src/addons/messageoutput/airnotifier/pages/devices/devices.module.ts new file mode 100644 index 000000000..fa7cb1aac --- /dev/null +++ b/src/addons/messageoutput/airnotifier/pages/devices/devices.module.ts @@ -0,0 +1,48 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonMessageOutputAirnotifierDevicesPage } from './devices'; + +const routes: Routes = [ + { + path: '', + component: AddonMessageOutputAirnotifierDevicesPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + FormsModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + declarations: [ + AddonMessageOutputAirnotifierDevicesPage, + ], + exports: [RouterModule], +}) +export class AddonMessageOutputAirnotifierDevicesPageModule {} diff --git a/src/addons/messageoutput/airnotifier/pages/devices/devices.ts b/src/addons/messageoutput/airnotifier/pages/devices/devices.ts new file mode 100644 index 000000000..ebd03ea82 --- /dev/null +++ b/src/addons/messageoutput/airnotifier/pages/devices/devices.ts @@ -0,0 +1,161 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { IonRefresher } from '@ionic/angular'; + +import { CoreDomUtils } from '@services/utils/dom'; +import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; +import { AddonMessageOutputAirnotifier, AddonMessageOutputAirnotifierDevice } from '../../services/airnotifier'; +import { CoreUtils } from '@services/utils/utils'; + +/** + * Page that displays the list of devices. + */ +@Component({ + selector: 'page-addon-message-output-airnotifier-devices', + templateUrl: 'devices.html', +}) +export class AddonMessageOutputAirnotifierDevicesPage implements OnInit, OnDestroy { + + devices?: AddonMessageOutputAirnotifierDeviceFormatted[] = []; + devicesLoaded = false; + + protected updateTimeout?: number; + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.fetchDevices(); + } + + /** + * Fetches the list of devices. + * + * @return Promise resolved when done. + */ + protected async fetchDevices(): Promise { + try { + const devices = await AddonMessageOutputAirnotifier.instance.getUserDevices(); + + this.devices = this.formatDevices(devices); + } catch (error) { + CoreDomUtils.instance.showErrorModal(error); + } finally { + this.devicesLoaded = true; + } + } + + /** + * Add some calculated data for devices. + * + * @param devices Devices to format. + * @return Formatted devices. + */ + protected formatDevices(devices: AddonMessageOutputAirnotifierDevice[]): AddonMessageOutputAirnotifierDeviceFormatted[] { + const formattedDevices: AddonMessageOutputAirnotifierDeviceFormatted[] = devices; + const pushId = CorePushNotifications.instance.getPushId(); + + // Convert enabled to boolean and search current device. + formattedDevices.forEach((device) => { + device.enable = !!device.enable; + device.current = !!(pushId && pushId == device.pushid); + }); + + return formattedDevices; + } + + /** + * 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 = window.setTimeout(() => { + this.updateTimeout = undefined; + this.updateDevices(); + }, 5000); + } + + /** + * Fetch devices. The purpose is to store the updated data, it won't be reflected in the view. + */ + protected async updateDevices(): Promise { + await CoreUtils.instance.ignoreErrors(AddonMessageOutputAirnotifier.instance.invalidateUserDevices()); + + await AddonMessageOutputAirnotifier.instance.getUserDevices(); + } + + /** + * Refresh the list of devices. + * + * @param refresher Refresher. + */ + async refreshDevices(refresher: CustomEvent): Promise { + try { + await CoreUtils.instance.ignoreErrors(AddonMessageOutputAirnotifier.instance.invalidateUserDevices()); + + await this.fetchDevices(); + } finally { + refresher?.detail.complete(); + } + } + + /** + * Enable or disable a certain device. + * + * @param device The device object. + * @param enable True to enable the device, false to disable it. + */ + async enableDevice(device: AddonMessageOutputAirnotifierDeviceFormatted, enable: boolean): Promise { + device.updating = true; + + try { + await AddonMessageOutputAirnotifier.instance.enableDevice(device.id, enable); + + // Update the list of devices since it was modified. + this.updateDevicesAfterDelay(); + } catch (error) { + // Show error and revert change. + CoreDomUtils.instance.showErrorModal(error); + 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(); + } + } + +} + +/** + * User device with some calculated data. + */ +type AddonMessageOutputAirnotifierDeviceFormatted = AddonMessageOutputAirnotifierDevice & { + current?: boolean; // Calculated in the app. Whether it's the current device. + updating?: boolean; // Calculated in the app. Whether the device enable is being updated right now. +}; diff --git a/src/addons/messageoutput/airnotifier/services/airnotifier.ts b/src/addons/messageoutput/airnotifier/services/airnotifier.ts new file mode 100644 index 000000000..954e29bcf --- /dev/null +++ b/src/addons/messageoutput/airnotifier/services/airnotifier.ts @@ -0,0 +1,190 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; + +import { CoreSites } from '@services/sites'; +import { CoreWSExternalWarning } from '@services/ws'; +import { CoreConstants } from '@/core/constants'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreError } from '@classes/errors/error'; +import { CoreWSError } from '@classes/errors/wserror'; +import { makeSingleton } from '@singletons'; +import { CoreEvents, CoreEventSiteData } from '@singletons/events'; + +const ROOT_CACHE_KEY = 'mmaMessageOutputAirnotifier:'; + +/** + * Service to handle Airnotifier message output. + */ +@Injectable({ providedIn: 'root' }) +export class AddonMessageOutputAirnotifierProvider { + + constructor() { + CoreEvents.on(CoreEvents.DEVICE_REGISTERED_IN_MOODLE, async (data: CoreEventSiteData) => { + // Get user devices to make Moodle send the devices data to Airnotifier. + this.getUserDevices(true, data.siteId); + }); + } + + /** + * Enables or disables a device. + * + * @param deviceId Device ID. + * @param enable True to enable, false to disable. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if success. + */ + async enableDevice(deviceId: number, enable: boolean, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + const data: AddonMessageOutputAirnotifierEnableDeviceWSParams = { + deviceid: deviceId, + enable: !!enable, + }; + + const result = await site.write( + 'message_airnotifier_enable_device', + data, + ); + + if (result.success) { + return; + } + + // Fail. Reject with warning message if any. + if (result.warnings?.length) { + throw new CoreWSError(result.warnings[0]); + } + + throw new CoreError('Error enabling device'); + } + + /** + * Get the cache key for the get user devices call. + * + * @return Cache key. + */ + protected getUserDevicesCacheKey(): string { + return ROOT_CACHE_KEY + 'userDevices'; + } + + /** + * Get user devices. + * + * @param ignoreCache Whether to ignore cache. + * @param siteId Site ID. If not defined, use current site. + * @return Promise resolved with the devices. + */ + async getUserDevices(ignoreCache?: boolean, siteId?: string): Promise { + + const site = await CoreSites.instance.getSite(siteId); + + const data: AddonMessageOutputAirnotifierGetUserDevicesWSParams = { + appid: CoreConstants.CONFIG.app_id, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getUserDevicesCacheKey(), + updateFrequency: CoreSite.FREQUENCY_RARELY, + }; + + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + const result = await site.read( + 'message_airnotifier_get_user_devices', + data, + preSets, + ); + + return result.devices; + } + + /** + * Invalidate get user devices. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when data is invalidated. + */ + async invalidateUserDevices(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return site.invalidateWsCacheForKey(this.getUserDevicesCacheKey()); + } + + /** + * Returns whether or not the plugin is enabled for the current site. + * + * @return True if enabled, false otherwise. + * @since 3.2 + */ + isEnabled(): boolean { + return CoreSites.instance.wsAvailableInCurrentSite('message_airnotifier_enable_device') && + CoreSites.instance.wsAvailableInCurrentSite('message_airnotifier_get_user_devices'); + } + +} + +export class AddonMessageOutputAirnotifier extends makeSingleton(AddonMessageOutputAirnotifierProvider) {} + +/** + * Device data returned by WS message_airnotifier_get_user_devices. + */ +export type AddonMessageOutputAirnotifierDevice = { + id: number; // Device id (in the message_airnotifier table). + appid: string; // The app id, something like com.moodle.moodlemobile. + name: string; // The device name, 'occam' or 'iPhone' etc. + model: string; // The device model 'Nexus4' or 'iPad1,1' etc. + platform: string; // The device platform 'iOS' or 'Android' etc. + version: string; // The device version '6.1.2' or '4.2.2' etc. + pushid: string; // The device PUSH token/key/identifier/registration id. + uuid: string; // The device UUID. + enable: number | boolean; // Whether the device is enabled or not. + timecreated: number; // Time created. + timemodified: number; // Time modified. +}; + +/** + * Params of message_airnotifier_enable_device WS. + */ +export type AddonMessageOutputAirnotifierEnableDeviceWSParams = { + deviceid: number; // The device id. + enable: boolean; // True for enable the device, false otherwise. +}; + +/** + * Result of WS message_airnotifier_enable_device. + */ +export type AddonMessageOutputAirnotifierEnableDeviceWSResponse = { + success: boolean; // True if success. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of message_airnotifier_get_user_devices WS. + */ +export type AddonMessageOutputAirnotifierGetUserDevicesWSParams = { + appid: string; // App unique id (usually a reversed domain). + userid?: number; // User id, 0 for current user. +}; + +/** + * Result of WS message_airnotifier_get_user_devices. + */ +export type AddonMessageOutputAirnotifierGetUserDevicesWSResponse = { + devices: AddonMessageOutputAirnotifierDevice[]; // List of devices. + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/addons/messageoutput/airnotifier/services/handlers/messageoutput.ts b/src/addons/messageoutput/airnotifier/services/handlers/messageoutput.ts new file mode 100644 index 000000000..c2668eb39 --- /dev/null +++ b/src/addons/messageoutput/airnotifier/services/handlers/messageoutput.ts @@ -0,0 +1,61 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; + +import { AddonMessageOutputHandler, AddonMessageOutputHandlerData } from '@addons/messageoutput/services/messageoutput-delegate'; +import { AddonMessageOutputAirnotifierProvider } from '../airnotifier'; +import { makeSingleton } from '@singletons'; + +/** + * Airnotifier message output handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonMessageOutputAirnotifierHandlerService implements AddonMessageOutputHandler { + + static readonly PAGE_NAME = 'messageoutput-airnotifier'; + + name = 'AddonMessageOutputAirnotifier'; + processorName = 'airnotifier'; + + constructor(private airnotifierProvider: AddonMessageOutputAirnotifierProvider) {} + + /** + * Whether or not the module is enabled for the site. + * + * @return True if enabled, false otherwise. + */ + async isEnabled(): Promise { + return this.airnotifierProvider.isEnabled(); + } + + /** + * Returns the data needed to render the handler. + * + * @param processor The processor object. + * @return Data. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getDisplayData(processor: Record): AddonMessageOutputHandlerData { + return { + priority: 600, + label: 'addon.messageoutput_airnotifier.processorsettingsdesc', + icon: 'fas-cog', + page: AddonMessageOutputAirnotifierHandlerService.PAGE_NAME, + }; + } + +} + +export class AddonMessageOutputAirnotifierHandler extends makeSingleton(AddonMessageOutputAirnotifierHandlerService) {} diff --git a/src/addons/messageoutput/messageoutput.module.ts b/src/addons/messageoutput/messageoutput.module.ts new file mode 100644 index 000000000..c12500d44 --- /dev/null +++ b/src/addons/messageoutput/messageoutput.module.ts @@ -0,0 +1,28 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; + +import { AddonMessageOutputAirnotifierModule } from './airnotifier/airnotifier.module'; + +@NgModule({ + declarations: [ + ], + imports: [ + AddonMessageOutputAirnotifierModule, + ], + providers: [ + ], +}) +export class AddonMessageOutputModule {} diff --git a/src/addons/messageoutput/services/messageoutput-delegate.ts b/src/addons/messageoutput/services/messageoutput-delegate.ts new file mode 100644 index 000000000..6a82ce5a6 --- /dev/null +++ b/src/addons/messageoutput/services/messageoutput-delegate.ts @@ -0,0 +1,93 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { Params } from '@angular/router'; + +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { makeSingleton } from '@singletons'; + +/** + * Interface that all message output handlers must implement. + */ +export interface AddonMessageOutputHandler extends CoreDelegateHandler { + /** + * The name of the processor. E.g. 'airnotifier'. + */ + processorName: string; + + /** + * Returns the data needed to render the handler. + * + * @param processor The processor object. + * @return Data. + */ + getDisplayData(processor: Record): AddonMessageOutputHandlerData; +} + +/** + * Data needed to render a message output handler. It's returned by the handler. + */ +export interface AddonMessageOutputHandlerData { + /** + * Handler's priority. + */ + priority: number; + + /** + * Name of the page to load for the handler. + */ + page: string; + + /** + * Label to display for the handler. + */ + label: string; + + /** + * Name of the icon to display for the handler. + */ + icon: string; + + /** + * Params to pass to the page. + */ + pageParams?: Params; +} + +/** + * Delegate to register processors (message/output) to be used in places like notification preferences. + */ +@Injectable({ providedIn: 'root' }) +export class AddonMessageOutputDelegateService extends CoreDelegate { + + protected handlerNameProperty = 'processorName'; + + constructor() { + super('AddonMessageOutputDelegate', true); + } + + /** + * Get the display data of the handler. + * + * @param processor The processor object. + * @return Data. + */ + getDisplayData(processor: Record): AddonMessageOutputHandlerData | undefined { + return this.executeFunctionOnEnabled( processor.name, 'getDisplayData', [processor]); + } + +} + +export class AddonMessageOutputDelegate extends makeSingleton(AddonMessageOutputDelegateService) {} diff --git a/tsconfig.app.json b/tsconfig.app.json index 69cd4cf31..feedfbd74 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -11,6 +11,7 @@ "webpack-env" ], "paths": { + "@addons/*": ["addons/*"], "@classes/*": ["core/classes/*"], "@components/*": ["core/components/*"], "@directives/*": ["core/directives/*"], diff --git a/tsconfig.json b/tsconfig.json index f94ebb411..0e32c7acc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,7 @@ "webpack-env" ], "paths": { + "@addons/*": ["addons/*"], "@classes/*": ["core/classes/*"], "@components/*": ["core/components/*"], "@directives/*": ["core/directives/*"], diff --git a/tsconfig.test.json b/tsconfig.test.json index 7c1e71c67..34d2a3698 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -15,6 +15,7 @@ "node" ], "paths": { + "@addons/*": ["addons/*"], "@classes/*": ["core/classes/*"], "@components/*": ["core/components/*"], "@directives/*": ["core/directives/*"],