MOBILE-3633 messageoutput: Implement messageoutput addons

main
Dani Palou 2021-01-12 10:32:39 +01:00
parent 11381458ad
commit fdbecff03e
12 changed files with 663 additions and 0 deletions

View File

@ -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 {}

View File

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

View File

@ -0,0 +1,27 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'addon.messageoutput_airnotifier.processorsettingsdesc' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!devicesLoaded" (ionRefresh)="refreshDevices($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="devicesLoaded">
<ion-list>
<ion-item class="ion-text-wrap" *ngFor="let device of devices">
<ion-label [class.core-bold]="device.current">
{{ device.name }} {{ device.model }} {{ device.platform }} {{ device.version }}
<span *ngIf="device.current">({{ 'core.currentdevice' | translate }})</span>
</ion-label>
<ion-spinner *ngIf="device.updating" slot="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,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 {}

View File

@ -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<void> {
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<void> {
await CoreUtils.instance.ignoreErrors(AddonMessageOutputAirnotifier.instance.invalidateUserDevices());
await AddonMessageOutputAirnotifier.instance.getUserDevices();
}
/**
* Refresh the list of devices.
*
* @param refresher Refresher.
*/
async refreshDevices(refresher: CustomEvent<IonRefresher>): Promise<void> {
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<void> {
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.
};

View File

@ -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<void> {
const site = await CoreSites.instance.getSite(siteId);
const data: AddonMessageOutputAirnotifierEnableDeviceWSParams = {
deviceid: deviceId,
enable: !!enable,
};
const result = await site.write<AddonMessageOutputAirnotifierEnableDeviceWSResponse>(
'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<AddonMessageOutputAirnotifierDevice[]> {
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<AddonMessageOutputAirnotifierGetUserDevicesWSResponse>(
'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<void> {
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[];
};

View File

@ -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<boolean> {
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<string, unknown>): AddonMessageOutputHandlerData {
return {
priority: 600,
label: 'addon.messageoutput_airnotifier.processorsettingsdesc',
icon: 'fas-cog',
page: AddonMessageOutputAirnotifierHandlerService.PAGE_NAME,
};
}
}
export class AddonMessageOutputAirnotifierHandler extends makeSingleton(AddonMessageOutputAirnotifierHandlerService) {}

View File

@ -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 {}

View File

@ -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<string, unknown>): 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<AddonMessageOutputHandler> {
protected handlerNameProperty = 'processorName';
constructor() {
super('AddonMessageOutputDelegate', true);
}
/**
* Get the display data of the handler.
*
* @param processor The processor object.
* @return Data.
*/
getDisplayData(processor: Record<string, unknown>): AddonMessageOutputHandlerData | undefined {
return this.executeFunctionOnEnabled(<string> processor.name, 'getDisplayData', [processor]);
}
}
export class AddonMessageOutputDelegate extends makeSingleton(AddonMessageOutputDelegateService) {}

View File

@ -11,6 +11,7 @@
"webpack-env" "webpack-env"
], ],
"paths": { "paths": {
"@addons/*": ["addons/*"],
"@classes/*": ["core/classes/*"], "@classes/*": ["core/classes/*"],
"@components/*": ["core/components/*"], "@components/*": ["core/components/*"],
"@directives/*": ["core/directives/*"], "@directives/*": ["core/directives/*"],

View File

@ -30,6 +30,7 @@
"webpack-env" "webpack-env"
], ],
"paths": { "paths": {
"@addons/*": ["addons/*"],
"@classes/*": ["core/classes/*"], "@classes/*": ["core/classes/*"],
"@components/*": ["core/components/*"], "@components/*": ["core/components/*"],
"@directives/*": ["core/directives/*"], "@directives/*": ["core/directives/*"],

View File

@ -15,6 +15,7 @@
"node" "node"
], ],
"paths": { "paths": {
"@addons/*": ["addons/*"],
"@classes/*": ["core/classes/*"], "@classes/*": ["core/classes/*"],
"@components/*": ["core/components/*"], "@components/*": ["core/components/*"],
"@directives/*": ["core/directives/*"], "@directives/*": ["core/directives/*"],