MOBILE-3592 user: Implement user delegate
parent
9ecbdd22b8
commit
d733488963
|
@ -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.
|
||||
};
|
|
@ -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 + ' ');
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue