From c3e2cdf7312e4e25f823b793c5dfb69339e8b91c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 12 Jun 2023 10:48:41 +0200 Subject: [PATCH] MOBILE-4261 user: Use initials instead of default avatar when possible --- .../components/onlineusers/onlineusers.scss | 93 ++++++++++++------- .../action/addon-mod-data-action.html | 6 +- .../components/site-picker/site-picker.ts | 2 +- .../user-avatar/core-user-avatar.html | 28 ++++-- .../components/user-avatar/user-avatar.scss | 22 ++++- .../components/user-avatar/user-avatar.ts | 25 ++++- .../choose-site-modal/choose-site-modal.html | 8 +- .../login/components/sites/sites.html | 18 ++-- src/core/features/login/login.scss | 4 + .../login/pages/reconnect/reconnect.html | 8 +- .../login/pages/reconnect/reconnect.ts | 34 ++++--- .../features/login/pages/sites/sites.html | 8 +- .../pages/space-usage/space-usage.html | 8 +- .../settings/pages/space-usage/space-usage.ts | 2 +- .../synchronization/synchronization.html | 8 +- .../pages/synchronization/synchronization.ts | 2 +- .../pages/choose-site/choose-site.html | 8 +- .../tag/components/feed/core-tag-feed.html | 5 +- .../features/user/services/user-helper.ts | 17 +++- src/core/features/user/services/user.ts | 10 ++ src/core/services/sites.ts | 16 ++-- src/theme/theme.light.scss | 1 + upgrade.txt | 4 + 23 files changed, 217 insertions(+), 120 deletions(-) diff --git a/src/addons/block/onlineusers/components/onlineusers/onlineusers.scss b/src/addons/block/onlineusers/components/onlineusers/onlineusers.scss index 9c3d9a059..1de16d878 100644 --- a/src/addons/block/onlineusers/components/onlineusers/onlineusers.scss +++ b/src/addons/block/onlineusers/components/onlineusers/onlineusers.scss @@ -1,51 +1,72 @@ @import "~theme/globals"; +:host { + --core-avatar-size: 30px; -:host .core-block-content ::ng-deep { - max-height: 200px; - overflow-y: auto; - .item-inner, - .input-wrapper { - overflow-y: visible; - align-self: start; - } + .core-block-content ::ng-deep { + max-height: 200px; + overflow-y: auto; + .item-inner, + .input-wrapper { + overflow-y: visible; + align-self: start; + } - .list { - margin-left: 0; - margin-right: 0; - -webkit-padding-start: 0; + .list { + margin-left: 0; + margin-right: 0; + -webkit-padding-start: 0; - li.listentry { - clear: both; - list-style-type: none; + li.listentry { + clear: both; + list-style-type: none; - .user { - @include float(start); - position: relative; - padding-bottom: 16px; + .user { + @include float(start); + position: relative; + padding-bottom: 16px; - .core-adapted-img-container { - display: inline; - @include margin-horizontal(0px, 8px); + .core-adapted-img-container { + display: inline; + @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 { - border-radius: 50%; + .message { + @include float(end); + margin-top: 3px; } - } - .message { - @include float(end); - margin-top: 3px; - } - - .uservisibility { // No support on the app. - display: none; + .uservisibility { // No support on the app. + display: none; + } } } - } - .info { - text-align: center; - } + .info { + text-align: center; + } + } } diff --git a/src/addons/mod/data/components/action/addon-mod-data-action.html b/src/addons/mod/data/components/action/addon-mod-data-action.html index 0351499aa..f1506fd9c 100644 --- a/src/addons/mod/data/components/action/addon-mod-data-action.html +++ b/src/addons/mod/data/components/action/addon-mod-data-action.html @@ -39,10 +39,8 @@ {{ entry.timecreated * 1000 | coreFormatDate }} {{ entry.timemodified * 1000 | coreFormatDate }} - - - + {{entry.fullname}} diff --git a/src/core/components/site-picker/site-picker.ts b/src/core/components/site-picker/site-picker.ts index 8cdec8a16..3bcb7d08e 100644 --- a/src/core/components/site-picker/site-picker.ts +++ b/src/core/components/site-picker/site-picker.ts @@ -63,7 +63,7 @@ export class CoreSitePickerComponent implements OnInit { site.fullNameAndSiteName = Translate.instant( 'core.fullnameandsitename', - { fullname: site.fullName, sitename: siteName }, + { fullname: site.fullname, sitename: siteName }, ); })); diff --git a/src/core/components/user-avatar/core-user-avatar.html b/src/core/components/user-avatar/core-user-avatar.html index 67eae965c..e968f63c2 100644 --- a/src/core/components/user-avatar/core-user-avatar.html +++ b/src/core/components/user-avatar/core-user-avatar.html @@ -1,14 +1,26 @@ - + + - + + + +
+ {{ initials }} +
- + +
+ + - + + diff --git a/src/core/components/user-avatar/user-avatar.scss b/src/core/components/user-avatar/user-avatar.scss index f2028ece8..5c41f6ca2 100644 --- a/src/core/components/user-avatar/user-avatar.scss +++ b/src/core/components/user-avatar/user-avatar.scss @@ -6,7 +6,7 @@ height: var(--core-avatar-size); img { - border-radius: 50%; + border-radius: var(--core-avatar-radius); width: var(--core-avatar-size); height: var(--core-avatar-size); max-width: var(--core-avatar-size); @@ -23,7 +23,7 @@ display: inline-block; position: relative; &:after { - border-radius: 50%; + border-radius: var(--core-avatar-radius); display: block; position: absolute; top: 0; @@ -62,6 +62,24 @@ 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 { diff --git a/src/core/components/user-avatar/user-avatar.ts b/src/core/components/user-avatar/user-avatar.ts index cd0f26495..5013019e9 100644 --- a/src/core/components/user-avatar/user-avatar.ts +++ b/src/core/components/user-avatar/user-avatar.ts @@ -20,6 +20,8 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { USER_PROFILE_PICTURE_UPDATED, CoreUserBasicData } from '@features/user/services/user'; import { CoreNavigator } from '@services/navigator'; import { CoreNetwork } from '@services/network'; +import { CoreUrl } from '@singletons/url'; +import { CoreUserHelper } from '@features/user/services/user-helper'; /** * 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() courseId?: number; @Input() checkOnline = false; // If want to check and show online status. + @Input() siteId?: string; avatarUrl?: string; + initials = ''; // 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. @@ -56,7 +60,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy { this.pictureObserver = CoreEvents.on( USER_PROFILE_PICTURE_UPDATED, (data) => { - if (data.userId == this.userId) { + if (data.userId === this.userId) { this.avatarUrl = data.picture; } }, @@ -68,6 +72,8 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy { * @inheritdoc */ ngOnInit(): void { + this.siteId = this.siteId || CoreSites.getCurrentSiteId(); + 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. */ @@ -88,12 +101,20 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy { 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))); - if (typeof profileUrl == 'string') { + if (typeof profileUrl === 'string') { this.avatarUrl = profileUrl; } 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.courseId = this.courseId || (this.user && this.user.courseid); } diff --git a/src/core/features/contentlinks/components/choose-site-modal/choose-site-modal.html b/src/core/features/contentlinks/components/choose-site-modal/choose-site-modal.html index eaf50abbb..84ac8273c 100644 --- a/src/core/features/contentlinks/components/choose-site-modal/choose-site-modal.html +++ b/src/core/features/contentlinks/components/choose-site-modal/choose-site-modal.html @@ -20,12 +20,10 @@ - - {{ 'core.pictureof' | translate:{$a: site.fullName} }} - + + -

{{site.fullName}}

+

{{site.fullname}}

diff --git a/src/core/features/login/components/sites/sites.html b/src/core/features/login/components/sites/sites.html index 8ddf94c02..b0e52ff2e 100644 --- a/src/core/features/login/components/sites/sites.html +++ b/src/core/features/login/components/sites/sites.html @@ -35,13 +35,11 @@ - - {{ 'core.pictureof' | translate:{$a: accountsList.currentSite.fullName} }} - + + -

{{accountsList.currentSite.fullName}}

+

{{accountsList.currentSite.fullname}}

@@ -75,12 +73,10 @@ - - {{ 'core.pictureof' | translate:{$a: site.fullName} }} - + + -

{{site.fullName}}

+

{{site.fullname}}

diff --git a/src/core/features/login/login.scss b/src/core/features/login/login.scss index ccbcd7e18..d31ea6cf3 100644 --- a/src/core/features/login/login.scss +++ b/src/core/features/login/login.scss @@ -75,6 +75,10 @@ text-decoration: underline; } + core-user-avatar.large-avatar { + --core-avatar-size: var(--core-large-avatar-size); + } + @if ($core-login-hide-forgot-password) { .core-login-forgotten-password { display: none; diff --git a/src/core/features/login/pages/reconnect/reconnect.html b/src/core/features/login/pages/reconnect/reconnect.html index 825c3315e..3fb029c24 100644 --- a/src/core/features/login/pages/reconnect/reconnect.html +++ b/src/core/features/login/pages/reconnect/reconnect.html @@ -20,8 +20,8 @@
- {{ 'core.pictureof' | translate:{$a: userFullName} }} + -

- +

+

{{siteUrl}}

diff --git a/src/core/features/login/pages/reconnect/reconnect.ts b/src/core/features/login/pages/reconnect/reconnect.ts index 65d696c93..ffb74ffa2 100644 --- a/src/core/features/login/pages/reconnect/reconnect.ts +++ b/src/core/features/login/pages/reconnect/reconnect.ts @@ -17,7 +17,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { CoreApp } from '@services/app'; 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 { CoreUtils } from '@services/utils/utils'; import { CoreLoginHelper } from '@features/login/services/login-helper'; @@ -47,9 +47,6 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy { credForm: FormGroup; siteUrl!: string; username!: string; - userFullName!: string; - userAvatar?: string; - siteName!: string; logoUrl?: string; identityProviders?: CoreSiteIdentityProvider[]; showForgottenPassword = true; @@ -58,6 +55,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy { isOAuth = false; isLoggedOut: boolean; siteId!: string; + siteInfo?: CoreSiteBasicInfo; showScanQR = false; showLoading = true; reconnectAttempts = 0; @@ -104,20 +102,30 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy { 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.userFullName = site.infos.fullname; - this.userAvatar = site.infos.userpictureurl; - this.siteUrl = site.infos.siteurl; - this.siteName = await site.getSiteName(); this.supportConfig = new CoreUserAuthenticatedSupportConfig(site); // If login was OAuth we should only reach this page if the OAuth method ID has changed. 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. - this.showUserAvatar = !!this.userAvatar && !sites.length; + this.showUserAvatar = !availableSites.length; this.checkSiteConfig(site); @@ -130,7 +138,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy { } /** - * Component destroyed. + * @inheritdoc */ ngOnDestroy(): void { this.viewLeft = true; @@ -191,10 +199,6 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy { 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); } diff --git a/src/core/features/login/pages/sites/sites.html b/src/core/features/login/pages/sites/sites.html index cb5d2afd3..438d20b27 100644 --- a/src/core/features/login/pages/sites/sites.html +++ b/src/core/features/login/pages/sites/sites.html @@ -33,12 +33,10 @@ - - {{ 'core.pictureof' | translate:{$a: site.fullName} }} - + + -

{{site.fullName}}

+

{{site.fullname}}

diff --git a/src/core/features/settings/pages/space-usage/space-usage.html b/src/core/features/settings/pages/space-usage/space-usage.html index 0f5040f75..bf28e9aa7 100644 --- a/src/core/features/settings/pages/space-usage/space-usage.html +++ b/src/core/features/settings/pages/space-usage/space-usage.html @@ -69,12 +69,10 @@ - - {{ 'core.pictureof' | translate:{$a: site.fullName} }} - + + -

{{site.fullName}}

+

{{site.fullname}}

{{ site.spaceUsage | coreBytesToSize }}
- - {{ 'core.pictureof' | translate:{$a: site.fullName} }} - + + -

{{site.fullName}}

+

{{site.fullname}}

{{ 'core.settings.logintosync' | translate }}

diff --git a/src/core/features/settings/pages/synchronization/synchronization.ts b/src/core/features/settings/pages/synchronization/synchronization.ts index a5c25a507..dd80abad3 100644 --- a/src/core/features/settings/pages/synchronization/synchronization.ts +++ b/src/core/features/settings/pages/synchronization/synchronization.ts @@ -84,7 +84,7 @@ export class CoreSettingsSynchronizationPage implements OnInit, OnDestroy { if (siteInfo) { siteEntry.siteUrl = siteInfo.siteurl; - siteEntry.fullName = siteInfo.fullname; + siteEntry.fullname = siteInfo.fullname; } }); diff --git a/src/core/features/sharedfiles/pages/choose-site/choose-site.html b/src/core/features/sharedfiles/pages/choose-site/choose-site.html index f931b9365..6091f5b76 100644 --- a/src/core/features/sharedfiles/pages/choose-site/choose-site.html +++ b/src/core/features/sharedfiles/pages/choose-site/choose-site.html @@ -18,12 +18,10 @@
- + + -

{{site.fullName}}

+

{{site.fullname}}

diff --git a/src/core/features/tag/components/feed/core-tag-feed.html b/src/core/features/tag/components/feed/core-tag-feed.html index 17b3de5ca..1223943fe 100644 --- a/src/core/features/tag/components/feed/core-tag-feed.html +++ b/src/core/features/tag/components/feed/core-tag-feed.html @@ -1,8 +1,7 @@ - - - + + diff --git a/src/core/features/user/services/user-helper.ts b/src/core/features/user/services/user-helper.ts index 015c9c8b2..1d45a65b4 100644 --- a/src/core/features/user/services/user-helper.ts +++ b/src/core/features/user/services/user-helper.ts @@ -17,7 +17,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { makeSingleton, Translate } from '@singletons'; -import { CoreUserRole } from './user'; +import { CoreUserProfile, CoreUserRole } from './user'; /** * Service that provides some features regarding users information. @@ -83,6 +83,21 @@ export class CoreUserHelperProvider { 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): 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); diff --git a/src/core/features/user/services/user.ts b/src/core/features/user/services/user.ts index 7484e800b..4e80ccf32 100644 --- a/src/core/features/user/services/user.ts +++ b/src/core/features/user/services/user.ts @@ -27,6 +27,8 @@ import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@servic import { CoreError } from '@classes/errors/error'; import { USERS_TABLE_NAME, CoreUserDBRecord } from './database/user'; import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; +import { CoreUserHelper } from './user-helper'; +import { CoreUrl } from '@singletons/url'; const ROOT_CACHE_KEY = 'mmUser:'; @@ -673,6 +675,14 @@ export class CoreUserProvider { 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; try { diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index 816c2eb81..f2e7b64bc 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -1260,9 +1260,11 @@ export class CoreSitesProvider { id: site.id, siteUrl: site.siteUrl, siteUrlWithoutProtocol: site.siteUrl.replace(/^https?:\/\//, '').toLowerCase(), - fullName: siteInfo?.fullname, + fullname: siteInfo?.fullname, + firstname: siteInfo?.firstname, + lastname: siteInfo?.lastname, siteName: siteInfo?.sitename, - avatar: siteInfo?.userpictureurl, + userpictureurl: siteInfo?.userpictureurl, siteHomeId: siteInfo?.siteid || 1, loggedOut: !!site.loggedOut, }; @@ -1300,8 +1302,8 @@ export class CoreSitesProvider { } // Finally use fullname. - textA = a.fullName?.toLowerCase().trim() || ''; - textB = b.fullName?.toLowerCase().trim() || ''; + textA = a.fullname?.toLowerCase().trim() || ''; + textB = b.fullname?.toLowerCase().trim() || ''; return textA.localeCompare(textB); }); @@ -2016,9 +2018,11 @@ export type CoreSiteBasicInfo = { id: string; // Site ID. siteUrl: string; // Site URL. 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. - avatar?: string; // User's avatar. badge?: number; // Badge to display in the site. siteHomeId?: number; // Site home ID. loggedOut: boolean; // If Site is logged out. diff --git a/src/theme/theme.light.scss b/src/theme/theme.light.scss index 1302c3a55..fa09f202c 100644 --- a/src/theme/theme.light.scss +++ b/src/theme/theme.light.scss @@ -330,6 +330,7 @@ html { --core-large-avatar-size: 90px; --core-avatar-size: var(--a11y-min-target-size); + --core-avatar-radius: 50%; --core-courseimage-on-course-size: 72px; --core-courseimage-radius: var(--medium-radius); diff --git a/upgrade.txt b/upgrade.txt index bf8043196..922dadc2e 100644 --- a/upgrade.txt +++ b/upgrade.txt @@ -1,6 +1,10 @@ This files describes API changes in the Moodle Mobile app, 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 === - CoreIconComponent has been removed after deprecation period: Use CoreFaIconDirective instead.