|
@ -35,9 +35,21 @@
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
|
|
||||||
.core-bar-button-image {
|
.core-bar-button-image {
|
||||||
|
--userpicture-padding: 4px;
|
||||||
@include margin-horizontal(null, 6px);
|
@include margin-horizontal(null, 6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Group avatar.
|
||||||
|
img.core-bar-button-image {
|
||||||
|
padding: var(--userpicture-padding);
|
||||||
|
width: var(--core-header-toolbar-button-image-size);
|
||||||
|
height: var(--core-header-toolbar-button-image-size);
|
||||||
|
max-width: var(--core-header-toolbar-button-image-size);
|
||||||
|
max-height: var(--core-header-toolbar-button-image-size);
|
||||||
|
border-radius: var(--core-avatar-radius);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
core-format-text {
|
core-format-text {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
@ -34,13 +34,14 @@
|
||||||
img {
|
img {
|
||||||
width: var(--icon-size);
|
width: var(--icon-size);
|
||||||
height: var(--icon-size);
|
height: var(--icon-size);
|
||||||
|
padding: 4px;
|
||||||
}
|
}
|
||||||
ion-icon {
|
ion-icon {
|
||||||
font-size: var(--icon-size);
|
font-size: var(--icon-size);
|
||||||
}
|
}
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
background: var(--background-color);
|
background: var(--background-color);
|
||||||
border-radius: var(--mdl-shape-borderRadius-xs);
|
border-radius: var(--core-avatar-radius);
|
||||||
@include margin(6px, 8px, 6px, 0px);
|
@include margin(6px, 8px, 6px, 0px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -119,7 +119,7 @@ export class AddonNotificationsProvider {
|
||||||
notification.notif = 1;
|
notification.notif = 1;
|
||||||
notification.read = notification.timeread > 0;
|
notification.read = notification.timeread > 0;
|
||||||
|
|
||||||
if (typeof notification.customdata == 'string') {
|
if (typeof notification.customdata === 'string') {
|
||||||
notification.customdata = CoreTextUtils.parseJSON<Record<string, string|number>>(notification.customdata, {});
|
notification.customdata = CoreTextUtils.parseJSON<Record<string, string|number>>(notification.customdata, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,9 +142,6 @@ export class AddonNotificationsProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const imgUrl = notification.customdata?.notificationpictureurl || notification.customdata?.notificationiconurl;
|
|
||||||
notification.imgUrl = imgUrl ? String(imgUrl) : undefined;
|
|
||||||
|
|
||||||
if (notification.useridfrom > 0) {
|
if (notification.useridfrom > 0) {
|
||||||
// Try to get the profile picture of the user.
|
// Try to get the profile picture of the user.
|
||||||
try {
|
try {
|
||||||
|
@ -155,6 +152,12 @@ export class AddonNotificationsProvider {
|
||||||
} catch {
|
} catch {
|
||||||
// Error getting user. This can happen if device is offline or the user is deleted.
|
// Error getting user. This can happen if device is offline or the user is deleted.
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Do not assign avatar for newlogin notifications.
|
||||||
|
if (notification.eventtype !== 'newlogin') {
|
||||||
|
const imgUrl = notification.customdata?.notificationpictureurl || notification.customdata?.notificationiconurl;
|
||||||
|
notification.imgUrl = imgUrl ? String(imgUrl) : undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return notification;
|
return notification;
|
||||||
|
|
|
@ -25,9 +25,9 @@ describe('CoreUserAvatarComponent', () => {
|
||||||
// Assert.
|
// Assert.
|
||||||
expect(nativeElement.innerHTML.trim()).not.toHaveLength(0);
|
expect(nativeElement.innerHTML.trim()).not.toHaveLength(0);
|
||||||
|
|
||||||
const image = nativeElement.querySelector('img');
|
const initials = nativeElement.querySelector('.userinitials');
|
||||||
expect(image).not.toBeNull();
|
expect(initials).not.toBeNull();
|
||||||
expect(image?.src).toEqual(document.location.href + 'assets/img/user-avatar.png');
|
expect(initials?.getAttribute('data-initials')?.trim()).toEqual('UNK');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,20 +7,15 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="!avatarUrl && initials">
|
<ng-container *ngIf="!avatarUrl && initials">
|
||||||
<div class="userinitials" *ngIf="linkProfile" [attr.aria-label]="'core.pictureof' | translate:{$a: fullname}"
|
<div class="userinitials" *ngIf="linkProfile" [attr.aria-label]="'core.pictureof' | translate:{$a: fullname}"
|
||||||
(ariaButtonClick)="gotoProfile($event)">
|
[title]="'core.pictureof' | translate:{$a: fullname}" (ariaButtonClick)="gotoProfile($event)" [attr.data-initials]="initials">
|
||||||
{{ initials }}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="userinitials" *ngIf="!linkProfile" [attr.aria-label]="'core.pictureof' | translate:{$a: fullname}" aria-hidden="true">
|
<div class="userinitials" *ngIf="!linkProfile" [attr.aria-label]="'core.pictureof' | translate:{$a: fullname}"
|
||||||
{{ initials }}
|
[title]="'core.pictureof' | translate:{$a: fullname}" role="img" [attr.data-initials]="initials" aria-hidden="true">
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="!avatarUrl && !initials">
|
<ng-container *ngIf="!avatarUrl && !initials"><!-- During loading -->
|
||||||
<img class="userpicture" *ngIf="linkProfile" src="assets/img/user-avatar.png" [alt]="'core.pictureof' | translate:{$a: fullname}"
|
<img class="userpicture_loading" src="assets/img/user-avatar.png" role="presentation" aria-hidden="true" alt="">
|
||||||
(ariaButtonClick)="gotoProfile($event)">
|
|
||||||
|
|
||||||
<img class="userpicture" *ngIf="!linkProfile" src="assets/img/user-avatar.png" [alt]="'core.pictureof' | translate:{$a: fullname}"
|
|
||||||
aria-hidden="true">
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<span *ngIf="checkOnline && isOnline()" class="contact-status online" role="status" [attr.aria-label]="'core.online' | translate">
|
<span *ngIf="checkOnline && isOnline()" class="contact-status online" role="status" [attr.aria-label]="'core.online' | translate">
|
||||||
|
|
|
@ -51,12 +51,8 @@
|
||||||
height: var(--core-header-toolbar-button-image-size);
|
height: var(--core-header-toolbar-button-image-size);
|
||||||
max-width: var(--core-header-toolbar-button-image-size);
|
max-width: var(--core-header-toolbar-button-image-size);
|
||||||
max-height: var(--core-header-toolbar-button-image-size);
|
max-height: var(--core-header-toolbar-button-image-size);
|
||||||
border-radius: 50%;
|
border-radius: var(--core-avatar-radius);
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
img {
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-status {
|
.contact-status {
|
||||||
|
@ -83,8 +79,14 @@
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
width: calc(var(--core-avatar-size) - var(--userpicture-padding) - var(--userpicture-padding));
|
width: calc(var(--core-avatar-size) - var(--userpicture-padding) - var(--userpicture-padding));
|
||||||
height: calc(var(--core-avatar-size) - var(--userpicture-padding) - var(--userpicture-padding));
|
height: calc(var(--core-avatar-size) - var(--userpicture-padding) - var(--userpicture-padding));
|
||||||
|
min-height: 0px;
|
||||||
|
min-width: 0px;
|
||||||
font-size: calc(var(--core-avatar-size)*0.3);
|
font-size: calc(var(--core-avatar-size)*0.3);
|
||||||
margin: var(--userpicture-padding);
|
margin: var(--userpicture-padding);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: attr(data-initials);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.large-avatar .userinitials {
|
&.large-avatar .userinitials {
|
||||||
|
|
|
@ -114,7 +114,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* Set fields from user.
|
* Set fields from user.
|
||||||
*/
|
*/
|
||||||
protected setFields(): void {
|
protected async setFields(): Promise<void> {
|
||||||
const profileUrl = this.profileUrl || (this.user && (this.user.profileimageurl || this.user.userprofileimageurl ||
|
const profileUrl = this.profileUrl || (this.user && (this.user.profileimageurl || this.user.userprofileimageurl ||
|
||||||
this.user.userpictureurl || this.user.profileimageurlsmall || (this.user.urls && this.user.urls.profileimage)));
|
this.user.userpictureurl || this.user.profileimageurlsmall || (this.user.urls && this.user.urls.profileimage)));
|
||||||
|
|
||||||
|
@ -124,16 +124,20 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
|
|
||||||
this.fullname = this.fullname || (this.user && (this.user.fullname || this.user.userfullname));
|
this.fullname = this.fullname || (this.user && (this.user.fullname || this.user.userfullname));
|
||||||
|
|
||||||
if (this.user) {
|
if (this.avatarUrl && CoreUrlUtils.isThemeImageUrl(this.avatarUrl)) {
|
||||||
this.initials = CoreUserHelper.getUserInitials(this.user);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.initials && this.avatarUrl && CoreUrlUtils.isThemeImageUrl(this.avatarUrl)) {
|
|
||||||
this.avatarUrl = undefined;
|
this.avatarUrl = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.userId = this.userId || (this.user && (this.user.userid || this.user.id));
|
this.userId = this.userId || (this.user && (this.user.userid || this.user.id));
|
||||||
this.courseId = this.courseId || (this.user && this.user.courseid);
|
this.courseId = this.courseId || (this.user && this.user.courseid);
|
||||||
|
|
||||||
|
this.initials =
|
||||||
|
await CoreUserHelper.getUserInitialsFromParts({
|
||||||
|
firstname: this.user?.firstname,
|
||||||
|
lastname: this.user?.lastname,
|
||||||
|
fullname: this.fullname,
|
||||||
|
userId: this.userId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
|
|
||||||
import { makeSingleton, Translate } from '@singletons';
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
import { CoreUserProfile, CoreUserRole } from './user';
|
import { CoreUser, CoreUserProfile, CoreUserRole } from './user';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that provides some features regarding users information.
|
* Service that provides some features regarding users information.
|
||||||
|
@ -145,7 +145,8 @@ export class CoreUserHelperProvider {
|
||||||
* Get the user initials.
|
* Get the user initials.
|
||||||
*
|
*
|
||||||
* @param user User object.
|
* @param user User object.
|
||||||
* @returns Promise resolved with the user data.
|
* @returns User initials.
|
||||||
|
* @deprecated since 4.4. Use getUserInitialsFromParts instead.
|
||||||
*/
|
*/
|
||||||
getUserInitials(user: Partial<CoreUserProfile>): string {
|
getUserInitials(user: Partial<CoreUserProfile>): string {
|
||||||
if (!user.firstname && !user.lastname) {
|
if (!user.firstname && !user.lastname) {
|
||||||
|
@ -156,6 +157,36 @@ export class CoreUserHelperProvider {
|
||||||
return (user.firstname?.charAt(0) || '') + (user.lastname?.charAt(0) || '');
|
return (user.firstname?.charAt(0) || '') + (user.lastname?.charAt(0) || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user initials.
|
||||||
|
*
|
||||||
|
* @param parts User name parts. Containing firstname, lastname, fullname and userId.
|
||||||
|
* @returns User initials.
|
||||||
|
*/
|
||||||
|
async getUserInitialsFromParts(parts: CoreUserNameParts): Promise<string> {
|
||||||
|
if (!parts.firstname && !parts.lastname) {
|
||||||
|
if (!parts.fullname && parts.userId) {
|
||||||
|
const user = await CoreUser.getProfile(parts.userId, undefined, true);
|
||||||
|
parts.fullname = user.fullname || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.fullname) {
|
||||||
|
const split = parts.fullname.split(' ');
|
||||||
|
|
||||||
|
parts.firstname = split[0];
|
||||||
|
if (split.length > 1) {
|
||||||
|
parts.lastname = split[split.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parts.firstname && !parts.lastname) {
|
||||||
|
return 'UNK';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (parts.firstname?.charAt(0) || '') + (parts.lastname?.charAt(0) || '');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translates legacy timezone names.
|
* Translates legacy timezone names.
|
||||||
*
|
*
|
||||||
|
@ -169,3 +200,5 @@ export class CoreUserHelperProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CoreUserHelper = makeSingleton(CoreUserHelperProvider);
|
export const CoreUserHelper = makeSingleton(CoreUserHelperProvider);
|
||||||
|
|
||||||
|
type CoreUserNameParts = { firstname?: string; lastname?: string; fullname?: string; userId?: number };
|
||||||
|
|
|
@ -26,7 +26,6 @@ import { CoreEvents, CoreEventSiteData, CoreEventUserDeletedData, CoreEventUserS
|
||||||
import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws';
|
import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws';
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
import { USERS_TABLE_NAME, CoreUserDBRecord } from './database/user';
|
import { USERS_TABLE_NAME, CoreUserDBRecord } from './database/user';
|
||||||
import { CoreUserHelper } from './user-helper';
|
|
||||||
import { CoreUrlUtils } from '@services/utils/url';
|
import { CoreUrlUtils } from '@services/utils/url';
|
||||||
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
|
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
|
||||||
import { CoreConstants } from '@/core/constants';
|
import { CoreConstants } from '@/core/constants';
|
||||||
|
@ -669,12 +668,9 @@ export class CoreUserProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not prefetch when initials are set and image is default.
|
// Do not prefetch when initials are set and image is default.
|
||||||
if ('firstname' in entry || 'lastname' in entry) {
|
if (imageUrl && CoreUrlUtils.isThemeImageUrl(imageUrl)) {
|
||||||
const initials = CoreUserHelper.getUserInitials(entry);
|
|
||||||
if (initials && imageUrl && CoreUrlUtils.isThemeImageUrl(imageUrl)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
treated[imageUrl] = true;
|
treated[imageUrl] = true;
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
@ -0,0 +1,70 @@
|
||||||
|
// (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 { CoreUserHelper } from '../services/user-helper';
|
||||||
|
|
||||||
|
describe('getUserInitialsFromParts', () => {
|
||||||
|
it('should return initials based on firstname and lastname', async () => {
|
||||||
|
const parts = {
|
||||||
|
firstname: 'John',
|
||||||
|
lastname: 'Doe',
|
||||||
|
fullname: '',
|
||||||
|
userId: 123,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await CoreUserHelper.getUserInitialsFromParts(parts);
|
||||||
|
|
||||||
|
expect(result).toEqual('JD');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return initials based on fullname if firstname and lastname are missing', async () => {
|
||||||
|
let parts = {
|
||||||
|
firstname: '',
|
||||||
|
lastname: '',
|
||||||
|
fullname: 'John Doe',
|
||||||
|
userId: 123,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = await CoreUserHelper.getUserInitialsFromParts(parts);
|
||||||
|
|
||||||
|
expect(result).toEqual('JD');
|
||||||
|
|
||||||
|
parts = {
|
||||||
|
firstname: '',
|
||||||
|
lastname: '',
|
||||||
|
fullname: 'John Fitzgerald Doe',
|
||||||
|
userId: 123,
|
||||||
|
};
|
||||||
|
|
||||||
|
result = await CoreUserHelper.getUserInitialsFromParts(parts);
|
||||||
|
|
||||||
|
expect(result).toEqual('JD');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return UNK string if empty parts', async () => {
|
||||||
|
const parts = {
|
||||||
|
firstname: '',
|
||||||
|
lastname: '',
|
||||||
|
fullname: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = await CoreUserHelper.getUserInitialsFromParts(parts);
|
||||||
|
|
||||||
|
expect(result).toEqual('UNK');
|
||||||
|
|
||||||
|
result = await CoreUserHelper.getUserInitialsFromParts({});
|
||||||
|
|
||||||
|
expect(result).toEqual('UNK');
|
||||||
|
});
|
||||||
|
});
|
|
@ -636,18 +636,6 @@ ion-content.limited-width > :not([slot]) {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-toolbar h1 img.core-bar-button-image,
|
|
||||||
ion-toolbar h1 .core-bar-button-image img {
|
|
||||||
padding: 4px;
|
|
||||||
--userpicture-padding: 4px;
|
|
||||||
width: var(--core-header-toolbar-button-image-size);
|
|
||||||
height: var(--core-header-toolbar-button-image-size);
|
|
||||||
max-width: var(--core-header-toolbar-button-image-size);
|
|
||||||
max-height: var(--core-header-toolbar-button-image-size);
|
|
||||||
border-radius: 50%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Radio.
|
// Radio.
|
||||||
ion-radio,
|
ion-radio,
|
||||||
input[type=radio],
|
input[type=radio],
|
||||||
|
|