|
@ -35,9 +35,21 @@
|
|||
padding: 0px;
|
||||
|
||||
.core-bar-button-image {
|
||||
--userpicture-padding: 4px;
|
||||
@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 {
|
||||
overflow: hidden;
|
||||
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 {
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
padding: 4px;
|
||||
}
|
||||
ion-icon {
|
||||
font-size: var(--icon-size);
|
||||
}
|
||||
padding: 0px;
|
||||
background: var(--background-color);
|
||||
border-radius: var(--mdl-shape-borderRadius-xs);
|
||||
border-radius: var(--core-avatar-radius);
|
||||
@include margin(6px, 8px, 6px, 0px);
|
||||
}
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ export class AddonNotificationsProvider {
|
|||
notification.notif = 1;
|
||||
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, {});
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
// Try to get the profile picture of the user.
|
||||
try {
|
||||
|
@ -155,6 +152,12 @@ export class AddonNotificationsProvider {
|
|||
} catch {
|
||||
// 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;
|
||||
|
|
|
@ -25,9 +25,9 @@ describe('CoreUserAvatarComponent', () => {
|
|||
// Assert.
|
||||
expect(nativeElement.innerHTML.trim()).not.toHaveLength(0);
|
||||
|
||||
const image = nativeElement.querySelector('img');
|
||||
expect(image).not.toBeNull();
|
||||
expect(image?.src).toEqual(document.location.href + 'assets/img/user-avatar.png');
|
||||
const initials = nativeElement.querySelector('.userinitials');
|
||||
expect(initials).not.toBeNull();
|
||||
expect(initials?.getAttribute('data-initials')?.trim()).toEqual('UNK');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -7,20 +7,15 @@
|
|||
</ng-container>
|
||||
<ng-container *ngIf="!avatarUrl && initials">
|
||||
<div class="userinitials" *ngIf="linkProfile" [attr.aria-label]="'core.pictureof' | translate:{$a: fullname}"
|
||||
(ariaButtonClick)="gotoProfile($event)">
|
||||
{{ initials }}
|
||||
[title]="'core.pictureof' | translate:{$a: fullname}" (ariaButtonClick)="gotoProfile($event)" [attr.data-initials]="initials">
|
||||
</div>
|
||||
|
||||
<div class="userinitials" *ngIf="!linkProfile" [attr.aria-label]="'core.pictureof' | translate:{$a: fullname}" aria-hidden="true">
|
||||
{{ initials }}
|
||||
<div class="userinitials" *ngIf="!linkProfile" [attr.aria-label]="'core.pictureof' | translate:{$a: fullname}"
|
||||
[title]="'core.pictureof' | translate:{$a: fullname}" role="img" [attr.data-initials]="initials" aria-hidden="true">
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!avatarUrl && !initials">
|
||||
<img class="userpicture" *ngIf="linkProfile" src="assets/img/user-avatar.png" [alt]="'core.pictureof' | translate:{$a: fullname}"
|
||||
(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 *ngIf="!avatarUrl && !initials"><!-- During loading -->
|
||||
<img class="userpicture_loading" src="assets/img/user-avatar.png" role="presentation" aria-hidden="true" alt="">
|
||||
</ng-container>
|
||||
|
||||
<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);
|
||||
max-width: 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;
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.contact-status {
|
||||
|
@ -83,8 +79,14 @@
|
|||
font-weight: normal;
|
||||
width: 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);
|
||||
margin: var(--userpicture-padding);
|
||||
|
||||
&::after {
|
||||
content: attr(data-initials);
|
||||
}
|
||||
}
|
||||
|
||||
&.large-avatar .userinitials {
|
||||
|
|
|
@ -114,7 +114,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
|
|||
/**
|
||||
* Set fields from user.
|
||||
*/
|
||||
protected setFields(): void {
|
||||
protected async setFields(): Promise<void> {
|
||||
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)));
|
||||
|
||||
|
@ -124,16 +124,20 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
|
|||
|
||||
this.fullname = this.fullname || (this.user && (this.user.fullname || this.user.userfullname));
|
||||
|
||||
if (this.user) {
|
||||
this.initials = CoreUserHelper.getUserInitials(this.user);
|
||||
}
|
||||
|
||||
if (this.initials && this.avatarUrl && CoreUrlUtils.isThemeImageUrl(this.avatarUrl)) {
|
||||
if (this.avatarUrl && CoreUrlUtils.isThemeImageUrl(this.avatarUrl)) {
|
||||
this.avatarUrl = undefined;
|
||||
}
|
||||
|
||||
this.userId = this.userId || (this.user && (this.user.userid || this.user.id));
|
||||
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 { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreUserProfile, CoreUserRole } from './user';
|
||||
import { CoreUser, CoreUserProfile, CoreUserRole } from './user';
|
||||
|
||||
/**
|
||||
* Service that provides some features regarding users information.
|
||||
|
@ -145,7 +145,8 @@ export class CoreUserHelperProvider {
|
|||
* Get the user initials.
|
||||
*
|
||||
* @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 {
|
||||
if (!user.firstname && !user.lastname) {
|
||||
|
@ -156,6 +157,36 @@ export class CoreUserHelperProvider {
|
|||
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.
|
||||
*
|
||||
|
@ -169,3 +200,5 @@ export class 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 { CoreError } from '@classes/errors/error';
|
||||
import { USERS_TABLE_NAME, CoreUserDBRecord } from './database/user';
|
||||
import { CoreUserHelper } from './user-helper';
|
||||
import { CoreUrlUtils } from '@services/utils/url';
|
||||
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
|
@ -669,11 +668,8 @@ export class CoreUserProvider {
|
|||
}
|
||||
|
||||
// Do not prefetch when initials are set and image is default.
|
||||
if ('firstname' in entry || 'lastname' in entry) {
|
||||
const initials = CoreUserHelper.getUserInitials(entry);
|
||||
if (initials && imageUrl && CoreUrlUtils.isThemeImageUrl(imageUrl)) {
|
||||
return;
|
||||
}
|
||||
if (imageUrl && CoreUrlUtils.isThemeImageUrl(imageUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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%;
|
||||
}
|
||||
|
||||
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.
|
||||
ion-radio,
|
||||
input[type=radio],
|
||||
|
|