MOBILE-3592 user: Implement user delegate

main
Dani Palou 2020-11-13 09:08:58 +01:00
parent 9ecbdd22b8
commit d733488963
5 changed files with 375 additions and 17 deletions

View File

@ -0,0 +1,283 @@
// (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 { NavController } from '@ionic/angular';
import { Subject, BehaviorSubject } from 'rxjs';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
import { CoreUtils } from '@services/utils/utils';
import { CoreEvents } from '@singletons/events';
import { CoreUserProfile } from './user';
/**
* Interface that all user profile handlers must implement.
*/
export interface CoreUserProfileHandler extends CoreDelegateHandler {
/**
* The highest priority is displayed first.
*/
priority: number;
/**
* A type should be specified among these:
* - TYPE_COMMUNICATION: will be displayed under the user avatar. Should have icon. Spinner not used.
* - TYPE_NEW_PAGE: will be displayed as a list of items. Should have icon. Spinner not used.
* Default value if none is specified.
* - TYPE_ACTION: will be displayed as a button and should not redirect to any state. Spinner use is recommended.
*/
type: string;
/**
* Whether or not the handler is enabled for a user.
*
* @param user User object.
* @param courseId Course ID where to show.
* @param navOptions Navigation options for the course.
* @param admOptions Admin options for the course.
* @return Whether or not the handler is enabled for a user.
*/
isEnabledForUser(user: CoreUserProfile, courseId: number, navOptions?: unknown, admOptions?: unknown): Promise<boolean>;
/**
* Returns the data needed to render the handler.
*
* @param user User object.
* @param courseId Course ID where to show.
* @return Data to be shown.
*/
getDisplayData(user: CoreUserProfile, courseId: number): CoreUserProfileHandlerData;
}
/**
* Data needed to render a user profile handler. It's returned by the handler.
*/
export interface CoreUserProfileHandlerData {
/**
* Title to display.
*/
title: string;
/**
* Name of the icon to display. Mandatory for TYPE_COMMUNICATION.
*/
icon?: string;
/**
* Additional class to add to the HTML.
*/
class?: string;
/**
* If enabled, element will be hidden. Only for TYPE_NEW_PAGE and TYPE_ACTION.
*/
hidden?: boolean;
/**
* If enabled will show an spinner. Only for TYPE_ACTION.
*/
spinner?: boolean;
/**
* Action to do when clicked.
*
* @param event Click event.
* @param Nav controller to use to navigate.
* @param user User object.
* @param courseId Course ID being viewed. If not defined, site context.
*/
action(event: Event, navCtrl: NavController, user: CoreUserProfile, courseId?: number): void;
}
/**
* Data returned by the delegate for each handler.
*/
export interface CoreUserProfileHandlerToDisplay {
/**
* Name of the handler.
*/
name?: string;
/**
* Data to display.
*/
data: CoreUserProfileHandlerData;
/**
* The highest priority is displayed first.
*/
priority?: number;
/**
* The type of the handler. See CoreUserProfileHandler.
*/
type: string;
}
/**
* Service to interact with plugins to be shown in user profile. Provides functions to register a plugin
* and notify an update in the data.
*/
@Injectable({
providedIn: 'root',
})
export class CoreUserDelegate extends CoreDelegate<CoreUserProfileHandler> {
/**
* User profile handler type for communication.
*/
static readonly TYPE_COMMUNICATION = 'communication';
/**
* User profile handler type for new page.
*/
static readonly TYPE_NEW_PAGE = 'newpage';
/**
* User profile handler type for actions.
*/
static readonly TYPE_ACTION = 'action';
/**
* Update handler information event.
*/
static readonly UPDATE_HANDLER_EVENT = 'CoreUserDelegate_update_handler_event';
protected featurePrefix = 'CoreUserDelegate_';
// Hold the handlers and the observable to notify them for each user.
protected userHandlers: {
[userId: number]: {
loaded: boolean; // Whether the handlers are loaded.
handlers: CoreUserProfileHandlerToDisplay[]; // List of handlers.
observable: Subject<CoreUserProfileHandlerToDisplay[]>; // Observale to notify the handlers.
};
} = {};
constructor() {
super('CoreUserDelegate', true);
CoreEvents.on<CoreUserUpdateHandlerData>(CoreUserDelegate.UPDATE_HANDLER_EVENT, (data) => {
if (!data || !data.handler || !this.userHandlers[data.userId]) {
return;
}
// Search the handler.
const handler = this.userHandlers[data.userId].handlers.find((userHandler) => userHandler.name == data.handler);
if (!handler) {
return;
}
// Update the data and notify.
Object.assign(handler.data, data.data);
this.userHandlers[data.userId].observable.next(this.userHandlers[data.userId].handlers);
});
}
/**
* Check if handlers are loaded.
*
* @return True if handlers are loaded, false otherwise.
*/
areHandlersLoaded(userId: number): boolean {
return this.userHandlers[userId]?.loaded;
}
/**
* Clear current user handlers.
*
* @param userId The user to clear.
*/
clearUserHandlers(userId: number): void {
const userData = this.userHandlers[userId];
if (userData) {
userData.handlers = [];
userData.observable.next([]);
userData.loaded = false;
}
}
/**
* Get the profile handlers for a user.
*
* @param user The user object.
* @param courseId The course ID.
* @return Resolved with the handlers.
*/
getProfileHandlersFor(user: CoreUserProfile, courseId: number): Subject<CoreUserProfileHandlerToDisplay[]> {
// Initialize the user handlers if it isn't initialized already.
if (!this.userHandlers[user.id]) {
this.userHandlers[user.id] = {
loaded: false,
handlers: [],
observable: new BehaviorSubject<CoreUserProfileHandlerToDisplay[]>([]),
};
}
this.calculateUserHandlers(user, courseId);
return this.userHandlers[user.id].observable;
}
/**
* Get the profile handlers for a user.
*
* @param user The user object.
* @param courseId The course ID.
* @return Promise resolved when done.
*/
protected async calculateUserHandlers(user: CoreUserProfile, courseId: number): Promise<void> {
// @todo: Get Course admin/nav options.
let navOptions;
let admOptions;
const userData = this.userHandlers[user.id];
userData.handlers = [];
await CoreUtils.instance.allPromises(Object.keys(this.enabledHandlers).map(async (name) => {
// Checks if the handler is enabled for the user.
const handler = this.handlers[name];
try {
const enabled = await handler.isEnabledForUser(user, courseId, navOptions, admOptions);
if (enabled) {
userData.handlers.push({
name: name,
data: handler.getDisplayData(user, courseId),
priority: handler.priority || 0,
type: handler.type || CoreUserDelegate.TYPE_NEW_PAGE,
});
}
} catch {
// Nothing to do here, it is not enabled for this user.
}
}));
// Sort them by priority.
userData.handlers.sort((a, b) => (b.priority || 0) - (a.priority || 0));
userData.loaded = true;
userData.observable.next(userData.handlers);
}
}
/**
* Data passed to UPDATE_HANDLER_EVENT event.
*/
export type CoreUserUpdateHandlerData = {
handler: string; // Name of the handler.
userId: number; // User affected.
data: Record<string, unknown>; // Data to set to the handler.
};

View File

@ -33,11 +33,11 @@ export class CoreUserHelperProvider {
* @param country Country.
* @return Formatted address.
*/
formatAddress(address: string, city: string, country: string): string {
formatAddress(address?: string, city?: string, country?: string): string {
const separator = Translate.instance.instant('core.listsep');
let values = [address, city, country];
values = values.filter((value) => value?.length > 0);
values = values.filter((value) => value && value.length > 0);
return values.join(separator + ' ');
}

View File

@ -21,7 +21,8 @@ import { CoreUtils } from '@services/utils/utils';
import { CoreUserOffline } from './user.offline';
import { CoreLogger } from '@singletons/logger';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { makeSingleton } from '@singletons/core.singletons';
import { makeSingleton, Translate } from '@singletons/core.singletons';
import { CoreEvents, CoreEventUserDeletedData } from '@singletons/events';
import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws';
import { CoreError } from '@classes/errors/error';
import { USERS_TABLE_NAME, CoreUserDBRecord } from './user.db';
@ -44,6 +45,25 @@ export class CoreUserProvider {
constructor() {
this.logger = CoreLogger.getInstance('CoreUserProvider');
CoreEvents.on<CoreEventUserDeletedData>(CoreEvents.USER_DELETED, (data) => {
// Search for userid in params.
let userId = 0;
if (data.params.userid) {
userId = data.params.userid;
} else if (data.params.userids) {
userId = data.params.userids[0];
} else if (data.params.field === 'id' && data.params.values && data.params.values.length) {
userId = data.params.values[0];
} else if (data.params.userlist && data.params.userlist.length) {
userId = data.params.userlist[0].userid;
}
if (userId > 0) {
this.deleteStoredUser(userId, data.siteId);
}
});
}
/**
@ -72,6 +92,32 @@ export class CoreUserProvider {
return !!site?.wsAvailable('core_enrol_search_users');
}
/**
* Check if WS to update profile picture is available in site.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with boolean: whether it's available.
* @since 3.2
*/
async canUpdatePicture(siteId?: string): Promise<boolean> {
const site = await CoreSites.instance.getSite(siteId);
return this.canUpdatePictureInSite(site);
}
/**
* Check if WS to search participants is available in site.
*
* @param site Site. If not defined, current site.
* @return Whether it's available.
* @since 3.2
*/
canUpdatePictureInSite(site?: CoreSite): boolean {
site = site || CoreSites.instance.getCurrentSite();
return !!site?.wsAvailable('core_user_update_picture');
}
/**
* Change the given user profile picture.
*
@ -199,7 +245,7 @@ export class CoreUserProvider {
courseId?: number,
forceLocal: boolean = false,
siteId?: string,
): Promise<CoreUserBasicData | CoreUserCourseProfile | CoreUserData> {
): Promise<CoreUserProfile> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
if (forceLocal) {
@ -291,7 +337,7 @@ export class CoreUserProvider {
throw new CoreError('Cannot retrieve user info.');
}
const user = users[0];
const user: CoreUserData | CoreUserCourseProfile = users[0];
if (user.country) {
user.country = CoreUtils.instance.getCountryName(user.country);
}
@ -759,6 +805,22 @@ export class CoreUserProvider {
export class CoreUser extends makeSingleton(CoreUserProvider) {}
/**
* Data passed to PROFILE_REFRESHED event.
*/
export type CoreUserProfileRefreshedData = {
courseId: number; // Course the user profile belongs to.
user: CoreUserProfile; // User affected.
};
/**
* Data passed to PROFILE_PICTURE_UPDATED event.
*/
export type CoreUserProfilePictureUpdatedData = {
userId: number; // User ID.
picture: string | undefined; // New picture URL.
};
/**
* Basic data of a user.
*/
@ -921,6 +983,11 @@ export type CoreUserCourseProfile = CoreUserData & {
enrolledcourses?: CoreUserEnrolledCourse[]; // Courses where the user is enrolled.
};
/**
* User data returned by getProfile.
*/
export type CoreUserProfile = (CoreUserBasicData & Partial<CoreUserData>) | CoreUserCourseProfile;
/**
* Params of core_user_update_picture WS.
*/

View File

@ -17,7 +17,7 @@ import { Md5 } from 'ts-md5/dist/md5';
import { CoreApp } from '@services/app';
import { CoreDB } from '@services/db';
import { CoreEvents } from '@singletons/events';
import { CoreEvents, CoreEventUserDeletedData } from '@singletons/events';
import { CoreFile } from '@services/file';
import {
CoreWS,
@ -588,7 +588,7 @@ export class CoreSite {
error.message = Translate.instance.instant('core.lostconnection');
} else if (error.errorcode === 'userdeleted') {
// User deleted, trigger event.
CoreEvents.trigger(CoreEvents.USER_DELETED, { params: data }, this.id);
CoreEvents.trigger<CoreEventUserDeletedData>(CoreEvents.USER_DELETED, { params: data }, this.id);
error.message = Translate.instance.instant('core.userdeleted');
throw new CoreWSError(error);

View File

@ -85,11 +85,11 @@ export class CoreEvents {
* @param siteId Site where to trigger the event. Undefined won't check the site.
* @return Observer to stop listening.
*/
static on(eventName: string, callBack: (value: unknown) => void, siteId?: string): CoreEventObserver {
static on<T = unknown>(eventName: string, callBack: (value: T) => void, siteId?: string): CoreEventObserver {
// If it's a unique event and has been triggered already, call the callBack.
// We don't need to create an observer because the event won't be triggered again.
if (this.uniqueEvents[eventName]) {
callBack(this.uniqueEvents[eventName].data);
callBack(<T> this.uniqueEvents[eventName].data);
// Return a fake observer to prevent errors.
return {
@ -103,10 +103,10 @@ export class CoreEvents {
if (typeof this.observables[eventName] == 'undefined') {
// No observable for this event, create a new one.
this.observables[eventName] = new Subject<unknown>();
this.observables[eventName] = new Subject<T>();
}
const subscription = this.observables[eventName].subscribe((value: {siteId?: string; [key: string]: unknown}) => {
const subscription = this.observables[eventName].subscribe((value: T & {siteId?: string}) => {
if (!siteId || value.siteId == siteId) {
callBack(value);
}
@ -132,8 +132,8 @@ export class CoreEvents {
* @param siteId Site where to trigger the event. Undefined won't check the site.
* @return Observer to stop listening.
*/
static onMultiple(eventNames: string[], callBack: (value: unknown) => void, siteId?: string): CoreEventObserver {
const observers = eventNames.map((name) => this.on(name, callBack, siteId));
static onMultiple<T = unknown>(eventNames: string[], callBack: (value: T) => void, siteId?: string): CoreEventObserver {
const observers = eventNames.map((name) => this.on<T>(name, callBack, siteId));
// Create and return a CoreEventObserver.
return {
@ -152,11 +152,11 @@ export class CoreEvents {
* @param data Data to pass to the observers.
* @param siteId Site where to trigger the event. Undefined means no Site.
*/
static trigger(eventName: string, data?: unknown, siteId?: string): void {
static trigger<T = unknown>(eventName: string, data?: T, siteId?: string): void {
this.logger.debug(`Event '${eventName}' triggered.`);
if (this.observables[eventName]) {
if (siteId) {
data = Object.assign(data || {}, { siteId });
Object.assign(data || {}, { siteId });
}
this.observables[eventName].next(data);
}
@ -169,14 +169,14 @@ export class CoreEvents {
* @param data Data to pass to the observers.
* @param siteId Site where to trigger the event. Undefined means no Site.
*/
static triggerUnique(eventName: string, data: unknown, siteId?: string): void {
static triggerUnique<T = unknown>(eventName: string, data: T, siteId?: string): void {
if (this.uniqueEvents[eventName]) {
this.logger.debug(`Unique event '${eventName}' ignored because it was already triggered.`);
} else {
this.logger.debug(`Unique event '${eventName}' triggered.`);
if (siteId) {
data = Object.assign(data || {}, { siteId });
Object.assign(data || {}, { siteId });
}
// Store the data so it can be passed to observers that register from now on.
@ -241,3 +241,11 @@ export type CoreEventCourseStatusChanged = {
courseId: number; // Course Id.
status: string;
};
/**
* Data passed to USER_DELETED event.
*/
export type CoreEventUserDeletedData = CoreEventSiteData & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
params: any; // Params sent to the WS that failed.
};