Merge pull request #4022 from crazyserver/MOBILE-4579

Mobile 4579
main
Dani Palou 2024-04-29 11:46:53 +02:00 committed by GitHub
commit eaedb0841a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 153 additions and 49 deletions

View File

@ -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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -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);
}

View File

@ -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;

View File

@ -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');
});
});

View File

@ -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">

View File

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

View File

@ -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,
});
}
/**

View File

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

View File

@ -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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -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');
});
});

View File

@ -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],