3
0

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

This commit is contained in:
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
src
addons
block/onlineusers/components/onlineusers
mod/data/components/action
core
components
features
contentlinks/components/choose-site-modal
login
settings/pages
sharedfiles/pages/choose-site
tag/components/feed
user/services
services
theme
upgrade.txt

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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