MOBILE-4261 user: Use initials instead of default avatar when possible

main
Pau Ferrer Ocaña 2023-06-12 10:48:41 +02:00
parent 28d744f337
commit c3e2cdf731
23 changed files with 217 additions and 120 deletions

View File

@ -1,51 +1,72 @@
@import "~theme/globals"; @import "~theme/globals";
:host {
--core-avatar-size: 30px;
:host .core-block-content ::ng-deep { .core-block-content ::ng-deep {
max-height: 200px; max-height: 200px;
overflow-y: auto; overflow-y: auto;
.item-inner, .item-inner,
.input-wrapper { .input-wrapper {
overflow-y: visible; overflow-y: visible;
align-self: start; align-self: start;
} }
.list { .list {
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
-webkit-padding-start: 0; -webkit-padding-start: 0;
li.listentry { li.listentry {
clear: both; clear: both;
list-style-type: none; list-style-type: none;
.user { .user {
@include float(start); @include float(start);
position: relative; position: relative;
padding-bottom: 16px; padding-bottom: 16px;
.core-adapted-img-container { .core-adapted-img-container {
display: inline; display: inline;
@include margin-horizontal(0px, 8px); @include margin-horizontal(0px, 0.25rem);
}
.userpicture {
border-radius: var(--core-avatar-radius);
width: var(--core-avatar-size);
height: var(--core-avatar-size);
}
.userinitials {
background-color: var(--gray-200);
vertical-align: middle;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--core-avatar-radius);
color: var(--gray-800);
font-weight: normal;
width: var(--core-avatar-size);
height: var(--core-avatar-size);
font-size: 0.7rem;
margin-right: 0.25rem;
text-decoration: none;
}
} }
.userpicture { .message {
border-radius: 50%; @include float(end);
margin-top: 3px;
} }
}
.message { .uservisibility { // No support on the app.
@include float(end); display: none;
margin-top: 3px; }
}
.uservisibility { // No support on the app.
display: none;
} }
} }
}
.info { .info {
text-align: center; text-align: center;
} }
}
} }

View File

@ -39,10 +39,8 @@
<span *ngIf="action == 'timeadded'">{{ entry.timecreated * 1000 | coreFormatDate }}</span> <span *ngIf="action == 'timeadded'">{{ entry.timecreated * 1000 | coreFormatDate }}</span>
<span *ngIf="action == 'timemodified'">{{ entry.timemodified * 1000 | coreFormatDate }}</span> <span *ngIf="action == 'timemodified'">{{ entry.timemodified * 1000 | coreFormatDate }}</span>
<a *ngIf="action == 'userpicture'" core-user-link [courseId]="database.course" [userId]="entry.userid" [title]="entry.fullname"> <core-user-avatar *ngIf="action == 'userpicture'" [user]="entry" slot="start" [courseId]="database.course" [userId]="entry.userid"
<img class="avatar-round" [src]="userPicture" [alt]="'core.pictureof' | translate:{$a: entry.fullname}" core-external-content [profileUrl]="userPicture"></core-user-avatar>
onError="this.src='assets/img/user-avatar.png'">
</a>
<a *ngIf="action == 'user' && entry" core-user-link [courseId]="database.course" [userId]="entry.userid" [title]="entry.fullname"> <a *ngIf="action == 'user' && entry" core-user-link [courseId]="database.course" [userId]="entry.userid" [title]="entry.fullname">
{{entry.fullname}} {{entry.fullname}}

View File

@ -63,7 +63,7 @@ export class CoreSitePickerComponent implements OnInit {
site.fullNameAndSiteName = Translate.instant( site.fullNameAndSiteName = Translate.instant(
'core.fullnameandsitename', 'core.fullnameandsitename',
{ fullname: site.fullName, sitename: siteName }, { fullname: site.fullname, sitename: siteName },
); );
})); }));

View File

@ -1,14 +1,26 @@
<img *ngIf="avatarUrl && linkProfile" [src]="avatarUrl" [alt]="'core.pictureof' | translate:{$a: fullname}" core-external-content <ng-container *ngIf="avatarUrl">
onError="this.src='assets/img/user-avatar.png'" (ariaButtonClick)="gotoProfile($event)"> <img *ngIf="linkProfile" [src]="avatarUrl" [alt]="'core.pictureof' | translate:{$a: fullname}" core-external-content
(error)="loadImageError()" (ariaButtonClick)="gotoProfile($event)" [siteId]="siteId">
<img *ngIf="avatarUrl && !linkProfile" [src]="avatarUrl" [alt]="'core.pictureof' | translate:{$a: fullname}" core-external-content <img *ngIf="!linkProfile" [src]="avatarUrl" [alt]="'core.pictureof' | translate:{$a: fullname}" core-external-content
onError="this.src='assets/img/user-avatar.png'" aria-hidden="true"> (error)="loadImageError()" aria-hidden="true" [siteId]="siteId">
</ng-container>
<ng-container *ngIf="!avatarUrl && initials">
<div class="userinitials" *ngIf="linkProfile" [attr.aria-label]="'core.pictureof' | translate:{$a: fullname}"
(ariaButtonClick)="gotoProfile($event)">
{{ initials }}
</div>
<img *ngIf="!avatarUrl && linkProfile" src="assets/img/user-avatar.png" [alt]="'core.pictureof' | translate:{$a: fullname}" <div class="userinitials" *ngIf="!linkProfile" [attr.aria-label]="'core.pictureof' | translate:{$a: fullname}" aria-hidden="true">
(ariaButtonClick)="gotoProfile($event)"> {{ initials }}
</div>
</ng-container>
<ng-container *ngIf="!avatarUrl && !initials">
<img *ngIf="linkProfile" src="assets/img/user-avatar.png" [alt]="'core.pictureof' | translate:{$a: fullname}"
(ariaButtonClick)="gotoProfile($event)">
<img *ngIf="!avatarUrl && !linkProfile" src="assets/img/user-avatar.png" [alt]="'core.pictureof' | translate:{$a: fullname}" <img *ngIf="!linkProfile" src="assets/img/user-avatar.png" [alt]="'core.pictureof' | translate:{$a: fullname}" aria-hidden="true">
aria-hidden="true"> </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">
</span> </span>

View File

@ -6,7 +6,7 @@
height: var(--core-avatar-size); height: var(--core-avatar-size);
img { img {
border-radius: 50%; border-radius: var(--core-avatar-radius);
width: var(--core-avatar-size); width: var(--core-avatar-size);
height: var(--core-avatar-size); height: var(--core-avatar-size);
max-width: var(--core-avatar-size); max-width: var(--core-avatar-size);
@ -23,7 +23,7 @@
display: inline-block; display: inline-block;
position: relative; position: relative;
&:after { &:after {
border-radius: 50%; border-radius: var(--core-avatar-radius);
display: block; display: block;
position: absolute; position: absolute;
top: 0; top: 0;
@ -62,6 +62,24 @@
background-color: var(--core-online-color); background-color: var(--core-online-color);
} }
} }
.userinitials {
background-color: var(--gray-200);
vertical-align: middle;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--core-avatar-radius);
color: var(--gray-800);
font-weight: normal;
width: var(--core-avatar-size);
height: var(--core-avatar-size);
font-size: calc(var(--core-avatar-size)*0.3);
}
&.large-avatar .userinitials {
margin-top: 8px;
}
} }
:host-context(.toolbar) .contact-status { :host-context(.toolbar) .contact-status {

View File

@ -20,6 +20,8 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { USER_PROFILE_PICTURE_UPDATED, CoreUserBasicData } from '@features/user/services/user'; import { USER_PROFILE_PICTURE_UPDATED, CoreUserBasicData } from '@features/user/services/user';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreNetwork } from '@services/network'; import { CoreNetwork } from '@services/network';
import { CoreUrl } from '@singletons/url';
import { CoreUserHelper } from '@features/user/services/user-helper';
/** /**
* Component to display a "user avatar". * Component to display a "user avatar".
@ -41,8 +43,10 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
@Input() userId?: number; // If provided or found it will be used to link the image to the profile. @Input() userId?: number; // If provided or found it will be used to link the image to the profile.
@Input() courseId?: number; @Input() courseId?: number;
@Input() checkOnline = false; // If want to check and show online status. @Input() checkOnline = false; // If want to check and show online status.
@Input() siteId?: string;
avatarUrl?: string; avatarUrl?: string;
initials = '';
// Variable to check if we consider this user online or not. // Variable to check if we consider this user online or not.
// @todo Use setting when available (see MDL-63972) so we can use site setting. // @todo Use setting when available (see MDL-63972) so we can use site setting.
@ -56,7 +60,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
this.pictureObserver = CoreEvents.on( this.pictureObserver = CoreEvents.on(
USER_PROFILE_PICTURE_UPDATED, USER_PROFILE_PICTURE_UPDATED,
(data) => { (data) => {
if (data.userId == this.userId) { if (data.userId === this.userId) {
this.avatarUrl = data.picture; this.avatarUrl = data.picture;
} }
}, },
@ -68,6 +72,8 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
* @inheritdoc * @inheritdoc
*/ */
ngOnInit(): void { ngOnInit(): void {
this.siteId = this.siteId || CoreSites.getCurrentSiteId();
this.setFields(); this.setFields();
} }
@ -81,6 +87,13 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
} }
} }
/**
* Avatar image loading error handler.
*/
loadImageError(): void {
this.avatarUrl = undefined;
}
/** /**
* Set fields from user. * Set fields from user.
*/ */
@ -88,12 +101,20 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
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)));
if (typeof profileUrl == 'string') { if (typeof profileUrl === 'string') {
this.avatarUrl = profileUrl; this.avatarUrl = profileUrl;
} }
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) {
this.initials = CoreUserHelper.getUserInitials(this.user);
}
if (this.initials && this.avatarUrl && CoreUrl.parse(this.avatarUrl)?.path?.startsWith('/theme/image.php')) {
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);
} }

View File

@ -20,12 +20,10 @@
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item *ngFor="let site of sites" (click)="siteClicked(site.id)" detail="false" button> <ion-item *ngFor="let site of sites" (click)="siteClicked(site.id)" detail="false" button>
<ion-avatar slot="start"> <core-user-avatar [user]="site" slot="start" [linkProfile]="false"></core-user-avatar>
<img [src]="site.avatar" core-external-content [siteId]="site.id"
alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}" onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<ion-label> <ion-label>
<p class="item-heading">{{site.fullName}}</p> <p class="item-heading">{{site.fullname}}</p>
<p> <p>
<core-format-text [text]="site.siteName" clean="true" [siteId]="site.id"></core-format-text> <core-format-text [text]="site.siteName" clean="true" [siteId]="site.id"></core-format-text>
</p> </p>

View File

@ -35,13 +35,11 @@
</ion-item-divider> </ion-item-divider>
<ion-item detail="false"> <ion-item detail="false">
<ion-avatar slot="start"> <core-user-avatar [user]="accountsList.currentSite" slot="start" [linkProfile]="false"
<img [src]="accountsList.currentSite.avatar" core-external-content [siteId]="accountsList.currentSite.id" [siteId]="accountsList.currentSite.id"></core-user-avatar>
alt="{{ 'core.pictureof' | translate:{$a: accountsList.currentSite.fullName} }}"
onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<ion-label> <ion-label>
<p class="item-heading">{{accountsList.currentSite.fullName}}</p> <p class="item-heading">{{accountsList.currentSite.fullname}}</p>
</ion-label> </ion-label>
<ion-icon color="success" name="fas-check"></ion-icon> <ion-icon color="success" name="fas-check"></ion-icon>
</ion-item> </ion-item>
@ -75,12 +73,10 @@
<!-- Template to render a list of sites. --> <!-- Template to render a list of sites. -->
<ng-template #siteList let-sites="sites"> <ng-template #siteList let-sites="sites">
<ion-item button *ngFor="let site of sites" (click)="login($event, site.id)" detail="true"> <ion-item button *ngFor="let site of sites" (click)="login($event, site.id)" detail="true">
<ion-avatar slot="start"> <core-user-avatar [user]="site" slot="start" [linkProfile]="false" [siteId]="site.id"></core-user-avatar>
<img [src]="site.avatar" core-external-content [siteId]="site.id" alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}"
onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<ion-label> <ion-label>
<p class="item-heading">{{site.fullName}}</p> <p class="item-heading">{{site.fullname}}</p>
</ion-label> </ion-label>
<ion-badge slot="end" *ngIf="!showDelete && site.badge" @coreShowHideAnimation> <ion-badge slot="end" *ngIf="!showDelete && site.badge" @coreShowHideAnimation>
<span aria-hidden="true">{{site.badge}}</span> <span aria-hidden="true">{{site.badge}}</span>

View File

@ -75,6 +75,10 @@
text-decoration: underline; text-decoration: underline;
} }
core-user-avatar.large-avatar {
--core-avatar-size: var(--core-large-avatar-size);
}
@if ($core-login-hide-forgot-password) { @if ($core-login-hide-forgot-password) {
.core-login-forgotten-password { .core-login-forgotten-password {
display: none; display: none;

View File

@ -20,8 +20,8 @@
<div class="list-item-limited-width"> <div class="list-item-limited-width">
<div class="ion-text-wrap ion-text-center ion-margin-bottom" [ngClass]="{'item-avatar-center': showUserAvatar}"> <div class="ion-text-wrap ion-text-center ion-margin-bottom" [ngClass]="{'item-avatar-center': showUserAvatar}">
<!-- Show user avatar. --> <!-- Show user avatar. -->
<img *ngIf="showUserAvatar" [src]="userAvatar" class="large-avatar" core-external-content [siteId]="siteId" <core-user-avatar class="large-avatar" *ngIf="showUserAvatar" [user]="siteInfo" [linkProfile]="false"
alt="{{ 'core.pictureof' | translate:{$a: userFullName} }}" onError="this.src='assets/img/user-avatar.png'"> [siteId]="siteId"></core-user-avatar>
<div class="core-login-site-logo" *ngIf="!showUserAvatar"> <div class="core-login-site-logo" *ngIf="!showUserAvatar">
<!-- Show site logo or a default image. --> <!-- Show site logo or a default image. -->
@ -29,8 +29,8 @@
<img *ngIf="!logoUrl" src="assets/img/login_logo.png" role="presentation" alt=""> <img *ngIf="!logoUrl" src="assets/img/login_logo.png" role="presentation" alt="">
</div> </div>
<p *ngIf="siteName" class="ion-padding core-sitename"> <p *ngIf="siteInfo?.siteName" class="ion-padding core-sitename">
<core-format-text [text]="siteName" [filter]="false"></core-format-text> <core-format-text [text]="siteInfo?.siteName" [filter]="false"></core-format-text>
</p> </p>
<p class="core-siteurl">{{siteUrl}}</p> <p class="core-siteurl">{{siteUrl}}</p>

View File

@ -17,7 +17,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { CoreApp } from '@services/app'; import { CoreApp } from '@services/app';
import { CoreNetwork } from '@services/network'; import { CoreNetwork } from '@services/network';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSiteBasicInfo, CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreLoginHelper } from '@features/login/services/login-helper';
@ -47,9 +47,6 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
credForm: FormGroup; credForm: FormGroup;
siteUrl!: string; siteUrl!: string;
username!: string; username!: string;
userFullName!: string;
userAvatar?: string;
siteName!: string;
logoUrl?: string; logoUrl?: string;
identityProviders?: CoreSiteIdentityProvider[]; identityProviders?: CoreSiteIdentityProvider[];
showForgottenPassword = true; showForgottenPassword = true;
@ -58,6 +55,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
isOAuth = false; isOAuth = false;
isLoggedOut: boolean; isLoggedOut: boolean;
siteId!: string; siteId!: string;
siteInfo?: CoreSiteBasicInfo;
showScanQR = false; showScanQR = false;
showLoading = true; showLoading = true;
reconnectAttempts = 0; reconnectAttempts = 0;
@ -104,20 +102,30 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
throw new CoreError('Invalid site'); throw new CoreError('Invalid site');
} }
this.siteUrl = site.getURL();
this.siteInfo = {
id: this.siteId,
siteUrl: this.siteUrl,
siteUrlWithoutProtocol: this.siteUrl.replace(/^https?:\/\//, '').toLowerCase(),
fullname: site.infos.fullname,
firstname: site.infos.firstname,
lastname: site.infos.lastname,
siteName: await site.getSiteName(),
userpictureurl: site.infos.userpictureurl,
loggedOut: true, // Not used.
};
this.username = site.infos.username; this.username = site.infos.username;
this.userFullName = site.infos.fullname;
this.userAvatar = site.infos.userpictureurl;
this.siteUrl = site.infos.siteurl;
this.siteName = await site.getSiteName();
this.supportConfig = new CoreUserAuthenticatedSupportConfig(site); this.supportConfig = new CoreUserAuthenticatedSupportConfig(site);
// If login was OAuth we should only reach this page if the OAuth method ID has changed. // If login was OAuth we should only reach this page if the OAuth method ID has changed.
this.isOAuth = site.isOAuth(); this.isOAuth = site.isOAuth();
const sites = await CoreLoginHelper.getAvailableSites(); const availableSites = await CoreLoginHelper.getAvailableSites();
// Show logo instead of avatar if it's a fixed site. // Show logo instead of avatar if it's a fixed site.
this.showUserAvatar = !!this.userAvatar && !sites.length; this.showUserAvatar = !availableSites.length;
this.checkSiteConfig(site); this.checkSiteConfig(site);
@ -130,7 +138,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
} }
/** /**
* Component destroyed. * @inheritdoc
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.viewLeft = true; this.viewLeft = true;
@ -191,10 +199,6 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
await CoreSites.checkApplication(this.siteConfig); await CoreSites.checkApplication(this.siteConfig);
// Check logoURL if user avatar is not set.
if (this.userAvatar?.startsWith(this.siteUrl + '/theme/image.php')) {
this.showUserAvatar = false;
}
this.logoUrl = CoreLoginHelper.getLogoUrl(this.siteConfig); this.logoUrl = CoreLoginHelper.getLogoUrl(this.siteConfig);
} }

View File

@ -33,12 +33,10 @@
</ion-item-divider> </ion-item-divider>
<ion-item button *ngFor="let site of sites" (click)="login($event, site.id)" detail="true"> <ion-item button *ngFor="let site of sites" (click)="login($event, site.id)" detail="true">
<ion-avatar slot="start"> <core-user-avatar [user]="site" slot="start" [linkProfile]="false" [siteId]="site.id"></core-user-avatar>
<img [src]="site.avatar" core-external-content [siteId]="site.id"
alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}" onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<ion-label> <ion-label>
<p class="item-heading">{{site.fullName}}</p> <p class="item-heading">{{site.fullname}}</p>
</ion-label> </ion-label>
<ion-badge slot="end" *ngIf="!showDelete && site.badge" @coreShowHideAnimation> <ion-badge slot="end" *ngIf="!showDelete && site.badge" @coreShowHideAnimation>
<span aria-hidden="true">{{site.badge}}</span> <span aria-hidden="true">{{site.badge}}</span>

View File

@ -69,12 +69,10 @@
<!-- Template to render a site space usage. --> <!-- Template to render a site space usage. -->
<ng-template #siteUsage let-site="site"> <ng-template #siteUsage let-site="site">
<ion-avatar slot="start"> <core-user-avatar [user]="site" slot="start" [linkProfile]="false" [siteId]="site.id"></core-user-avatar>
<img [src]="site.avatar" core-external-content [siteId]="site.id" alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}"
onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<ion-label class="ion-text-wrap"> <ion-label class="ion-text-wrap">
<p class="item-heading">{{site.fullName}}</p> <p class="item-heading">{{site.fullname}}</p>
<ion-badge color="light" *ngIf="site.spaceUsage !== undefined">{{ site.spaceUsage | coreBytesToSize }}</ion-badge> <ion-badge color="light" *ngIf="site.spaceUsage !== undefined">{{ site.spaceUsage | coreBytesToSize }}</ion-badge>
</ion-label> </ion-label>
<ion-button fill="clear" color="danger" slot="end" (click)="deleteSiteStorage(site)" <ion-button fill="clear" color="danger" slot="end" (click)="deleteSiteStorage(site)"

View File

@ -70,7 +70,7 @@ export class CoreSettingsSpaceUsagePage implements OnInit, OnDestroy {
if (siteInfo) { if (siteInfo) {
siteEntry.siteUrl = siteInfo.siteurl; siteEntry.siteUrl = siteInfo.siteurl;
siteEntry.fullName = siteInfo.fullname; siteEntry.fullname = siteInfo.fullname;
} }
}); });
} }

View File

@ -94,12 +94,10 @@
<!-- Template to render a site to sync. --> <!-- Template to render a site to sync. -->
<ng-template #siteSync let-site="site"> <ng-template #siteSync let-site="site">
<ion-avatar slot="start"> <core-user-avatar [user]="site" slot="start" [linkProfile]="false" [siteId]="site.id"></core-user-avatar>
<img [src]="site.avatar" core-external-content [siteId]="site.id" alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}"
onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<ion-label> <ion-label>
<p class="item-heading">{{site.fullName}}</p> <p class="item-heading">{{site.fullname}}</p>
<p class="text-danger" *ngIf="site.loggedOut">{{ 'core.settings.logintosync' | translate }}</p> <p class="text-danger" *ngIf="site.loggedOut">{{ 'core.settings.logintosync' | translate }}</p>
</ion-label> </ion-label>
<core-button-with-spinner [loading]="isSynchronizing(site.id)" slot="end" *ngIf="!site.loggedOut"> <core-button-with-spinner [loading]="isSynchronizing(site.id)" slot="end" *ngIf="!site.loggedOut">

View File

@ -84,7 +84,7 @@ export class CoreSettingsSynchronizationPage implements OnInit, OnDestroy {
if (siteInfo) { if (siteInfo) {
siteEntry.siteUrl = siteInfo.siteurl; siteEntry.siteUrl = siteInfo.siteurl;
siteEntry.fullName = siteInfo.fullname; siteEntry.fullname = siteInfo.fullname;
} }
}); });

View File

@ -18,12 +18,10 @@
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item *ngFor="let site of sites" (click)="storeInSite(site.id)" detail="false" button> <ion-item *ngFor="let site of sites" (click)="storeInSite(site.id)" detail="false" button>
<ion-avatar slot="start" aria-hidden="true"> <core-user-avatar [user]="site" slot="start" [linkProfile]="false" [siteId]="site.id"></core-user-avatar>
<img [src]="site.avatar" core-external-content [siteId]="site.id"
alt="{{ 'core.pictureof' | translate:{$a: site.fullname} }}" onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<ion-label> <ion-label>
<p class="item-heading">{{site.fullName}}</p> <p class="item-heading">{{site.fullname}}</p>
<p> <p>
<core-format-text clean="true" [text]="site.siteName" [siteId]="site.id"></core-format-text> <core-format-text clean="true" [text]="site.siteName" [siteId]="site.id"></core-format-text>
</p> </p>

View File

@ -1,8 +1,7 @@
<ion-card *ngFor="let item of items"> <ion-card *ngFor="let item of items">
<ion-item class="ion-text-wrap" [href]="item.url" core-link [capture]="true"> <ion-item class="ion-text-wrap" [href]="item.url" core-link [capture]="true">
<ion-avatar slot="start" *ngIf="item.avatarUrl"> <core-user-avatar *ngIf="item.avatarUrl" [profileUrl]="item.avatarUrl" slot="start" [linkProfile]="false"></core-user-avatar>
<img [src]="item.avatarUrl" core-external-content alt="" role="presentation" onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<core-mod-icon *ngIf="item.iconUrl" [modicon]="item.iconUrl" slot="start" [showAlt]="false"> <core-mod-icon *ngIf="item.iconUrl" [modicon]="item.iconUrl" slot="start" [showAlt]="false">
</core-mod-icon> </core-mod-icon>
<ion-label> <ion-label>

View File

@ -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 { CoreUserRole } from './user'; import { CoreUserProfile, CoreUserRole } from './user';
/** /**
* Service that provides some features regarding users information. * Service that provides some features regarding users information.
@ -83,6 +83,21 @@ export class CoreUserHelperProvider {
await CoreNavigator.navigate('/user/completeprofile', { params: { siteId }, reset: true }); await CoreNavigator.navigate('/user/completeprofile', { params: { siteId }, reset: true });
} }
/**
* Get the user initials.
*
* @param user User object.
* @returns Promise resolved with the user data.
*/
getUserInitials(user: Partial<CoreUserProfile>): string {
if (!user.firstname && !user.lastname) {
// @TODO: Use local info or check WS to get initials from.
return '';
}
return (user.firstname?.charAt(0) || '') + (user.lastname?.charAt(0) || '');
}
} }
export const CoreUserHelper = makeSingleton(CoreUserHelperProvider); export const CoreUserHelper = makeSingleton(CoreUserHelperProvider);

View File

@ -27,6 +27,8 @@ import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@servic
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 { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
import { CoreUserHelper } from './user-helper';
import { CoreUrl } from '@singletons/url';
const ROOT_CACHE_KEY = 'mmUser:'; const ROOT_CACHE_KEY = 'mmUser:';
@ -673,6 +675,14 @@ export class CoreUserProvider {
return; return;
} }
// 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 && CoreUrl.parse(imageUrl)?.path === '/theme/image.php') {
return;
}
}
treated[imageUrl] = true; treated[imageUrl] = true;
try { try {

View File

@ -1260,9 +1260,11 @@ export class CoreSitesProvider {
id: site.id, id: site.id,
siteUrl: site.siteUrl, siteUrl: site.siteUrl,
siteUrlWithoutProtocol: site.siteUrl.replace(/^https?:\/\//, '').toLowerCase(), siteUrlWithoutProtocol: site.siteUrl.replace(/^https?:\/\//, '').toLowerCase(),
fullName: siteInfo?.fullname, fullname: siteInfo?.fullname,
firstname: siteInfo?.firstname,
lastname: siteInfo?.lastname,
siteName: siteInfo?.sitename, siteName: siteInfo?.sitename,
avatar: siteInfo?.userpictureurl, userpictureurl: siteInfo?.userpictureurl,
siteHomeId: siteInfo?.siteid || 1, siteHomeId: siteInfo?.siteid || 1,
loggedOut: !!site.loggedOut, loggedOut: !!site.loggedOut,
}; };
@ -1300,8 +1302,8 @@ export class CoreSitesProvider {
} }
// Finally use fullname. // Finally use fullname.
textA = a.fullName?.toLowerCase().trim() || ''; textA = a.fullname?.toLowerCase().trim() || '';
textB = b.fullName?.toLowerCase().trim() || ''; textB = b.fullname?.toLowerCase().trim() || '';
return textA.localeCompare(textB); return textA.localeCompare(textB);
}); });
@ -2016,9 +2018,11 @@ export type CoreSiteBasicInfo = {
id: string; // Site ID. id: string; // Site ID.
siteUrl: string; // Site URL. siteUrl: string; // Site URL.
siteUrlWithoutProtocol: string; // Site URL without protocol. siteUrlWithoutProtocol: string; // Site URL without protocol.
fullName?: string; // User's full name. fullname?: string; // User's full name.
firstname?: string; // User's first name.
lastname?: string; // User's last name.
userpictureurl?: string; // User avatar.
siteName?: string; // Site's name. siteName?: string; // Site's name.
avatar?: string; // User's avatar.
badge?: number; // Badge to display in the site. badge?: number; // Badge to display in the site.
siteHomeId?: number; // Site home ID. siteHomeId?: number; // Site home ID.
loggedOut: boolean; // If Site is logged out. loggedOut: boolean; // If Site is logged out.

View File

@ -330,6 +330,7 @@ html {
--core-large-avatar-size: 90px; --core-large-avatar-size: 90px;
--core-avatar-size: var(--a11y-min-target-size); --core-avatar-size: var(--a11y-min-target-size);
--core-avatar-radius: 50%;
--core-courseimage-on-course-size: 72px; --core-courseimage-on-course-size: 72px;
--core-courseimage-radius: var(--medium-radius); --core-courseimage-radius: var(--medium-radius);

View File

@ -1,6 +1,10 @@
This files describes API changes in the Moodle Mobile app, This files describes API changes in the Moodle Mobile app,
information provided here is intended especially for developers. information provided here is intended especially for developers.
=== 4.3.0 ===
- CoreSiteBasicInfo fullName attribute has changed to fullname and avatar to userpictureurl to match user fields.
=== 4.2.0 === === 4.2.0 ===
- CoreIconComponent has been removed after deprecation period: Use CoreFaIconDirective instead. - CoreIconComponent has been removed after deprecation period: Use CoreFaIconDirective instead.