MOBILE-3807 user: Edit avatar only from details

main
Pau Ferrer Ocaña 2021-10-21 15:44:53 +02:00
parent 434a2a90f2
commit 0bbfb898d1
7 changed files with 225 additions and 162 deletions

View File

@ -18,7 +18,6 @@
}
}
@if ($core-user-hide-siteinfo) {
.core-usermenu-siteinfo {
display: none;

View File

@ -3,7 +3,7 @@
<ion-buttons slot="start">
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<h1 *ngIf="title">{{ title }}</h1>
<h1>{{ 'core.user.details' | translate }}</h1>
</ion-toolbar>
</ion-header>
<ion-content>
@ -12,6 +12,25 @@
</ion-refresher>
<core-loading [hideUntil]="userLoaded">
<ion-list *ngIf="user">
<ion-item class="ion-text-center core-user-profile-maininfo">
<core-user-avatar [user]="user" [userId]="user.id" [linkProfile]="false" [checkOnline]="true">
<ion-button
class="edit-avatar"
*ngIf="canChangeProfilePicture"
(click)="changeProfilePicture()"
[attr.aria-label]="'core.user.newpicture' | translate"
fill="clear"
color="dark"
>
<ion-icon slot="icon-only" name="fas-pen" aria-hidden="true"></ion-icon>
</ion-button>
</core-user-avatar>
<ion-label>
<h2>{{ user.fullname }}</h2>
<p *ngIf="user.address"><ion-icon name="fas-map-marker-alt" [attr.aria-hidden]="true"></ion-icon> {{ user.address }}</p>
</ion-label>
</ion-item>
<ion-item-group *ngIf="hasContact">
<ion-item-divider><ion-label><h2>{{ 'core.user.contact' | translate}}</h2></ion-label></ion-item-divider>
<ion-item class="ion-text-wrap" *ngIf="user.email">

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { SafeUrl } from '@angular/platform-browser';
import { IonRefresher } from '@ionic/angular';
@ -20,10 +20,15 @@ import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { CoreEvents } from '@singletons/events';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreUser, CoreUserProfile, CoreUserProvider } from '@features/user/services/user';
import { CoreUserHelper } from '@features/user/services/user-helper';
import { CoreNavigator } from '@services/navigator';
import { CoreIonLoadingElement } from '@classes/ion-loading';
import { CoreSite } from '@classes/site';
import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuploader-helper';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { Translate } from '@singletons';
/**
* Page that displays info about a user.
@ -31,11 +36,9 @@ import { CoreNavigator } from '@services/navigator';
@Component({
selector: 'page-core-user-about',
templateUrl: 'about.html',
styleUrls: ['about.scss'],
})
export class CoreUserAboutPage implements OnInit {
protected userId!: number;
protected siteId: string;
export class CoreUserAboutPage implements OnInit, OnDestroy {
courseId!: number;
userLoaded = false;
@ -45,20 +48,46 @@ export class CoreUserAboutPage implements OnInit {
title?: string;
formattedAddress?: string;
encodedAddress?: SafeUrl;
canChangeProfilePicture = false;
protected userId!: number;
protected site!: CoreSite;
protected obsProfileRefreshed?: CoreEventObserver;
constructor() {
this.siteId = CoreSites.getCurrentSiteId();
try {
this.site = CoreSites.getRequiredCurrentSite();
} catch (error) {
CoreDomUtils.showErrorModal(error);
CoreNavigator.back();
return;
}
this.obsProfileRefreshed = CoreEvents.on(CoreUserProvider.PROFILE_REFRESHED, (data) => {
if (!this.user || !data.user) {
return;
}
this.user.email = data.user.email;
this.user.address = CoreUserHelper.formatAddress('', data.user.city, data.user.country);
}, CoreSites.getCurrentSiteId());
}
/**
* On init.
*
* @return Promise resolved when done.
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
this.userId = CoreNavigator.getRouteNumberParam('userId') || 0;
this.courseId = CoreNavigator.getRouteNumberParam('courseId') || 0;
// Allow to change the profile image only in the app profile page.
this.canChangeProfilePicture =
!this.courseId &&
this.userId == this.site.getUserId() &&
this.site.canUploadFiles() &&
!CoreUser.isUpdatePictureDisabledInSite(this.site);
this.fetchUser().finally(() => {
this.userLoaded = true;
});
@ -83,11 +112,85 @@ export class CoreUserAboutPage implements OnInit {
this.user = user;
this.title = user.fullname;
this.user.address = CoreUserHelper.formatAddress('', user.city, user.country);
await this.checkUserImageUpdated();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.user.errorloaduser', true);
}
}
/**
* Check if current user image has changed.
*
* @return Promise resolved when done.
*/
protected async checkUserImageUpdated(): Promise<void> {
if (!this.site || !this.site.getInfo() || !this.user) {
return;
}
if (this.userId != this.site.getUserId() || !this.isUserAvatarDirty()) {
// Not current user or hasn't changed.
return;
}
// The current user image received is different than the one stored in site info. Assume the image was updated.
// Update the site info to get the right avatar in there.
try {
await CoreSites.updateSiteInfo(this.site.getId());
} catch {
// Cannot update site info. Assume the profile image is the right one.
CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, {
userId: this.userId,
picture: this.user.profileimageurl,
}, this.site.getId());
}
if (this.isUserAvatarDirty()) {
// The image is still different, this means that the good one is the one in site info.
await this.refreshUser();
} else {
// Now they're the same, send event to use the right avatar in the rest of the app.
CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, {
userId: this.userId,
picture: this.user.profileimageurl,
}, this.site.getId());
}
}
/**
* Opens dialog to change profile picture.
*/
async changeProfilePicture(): Promise<void> {
const maxSize = -1;
const title = Translate.instant('core.user.newpicture');
const mimetypes = CoreMimetypeUtils.getGroupMimeInfo('image', 'mimetypes');
let modal: CoreIonLoadingElement | undefined;
try {
const result = await CoreFileUploaderHelper.selectAndUploadFile(maxSize, title, mimetypes);
modal = await CoreDomUtils.showModalLoading('core.sending', true);
const profileImageURL = await CoreUser.changeProfilePicture(result.itemid, this.userId, this.site.getId());
CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, {
userId: this.userId,
picture: profileImageURL,
}, this.site.getId());
CoreSites.updateSiteInfo(this.site.getId());
this.refreshUser();
} catch (error) {
CoreDomUtils.showErrorModal(error);
} finally {
modal?.dismiss();
}
}
/**
* Refresh the user data.
*
@ -106,8 +209,52 @@ export class CoreUserAboutPage implements OnInit {
courseId: this.courseId,
userId: this.userId,
user: this.user,
}, this.siteId);
}, this.site.getId());
}
}
/**
* Check whether the user avatar is not up to date with site info.
*
* @return Whether the user avatar differs from site info cache.
*/
protected isUserAvatarDirty(): boolean {
if (!this.user || !this.site) {
return false;
}
const courseAvatarUrl = this.normalizeAvatarUrl(this.user.profileimageurl);
const siteAvatarUrl = this.normalizeAvatarUrl(this.site.getInfo()?.userpictureurl);
return courseAvatarUrl !== siteAvatarUrl;
}
/**
* Normalize an avatar url regardless of theme.
*
* Given that the default image is the only one that can be changed per theme, any other url will stay the same. Note that
* the values returned by this function may not be valid urls, given that they are intended for string comparison.
*
* @param avatarUrl Avatar url.
* @return Normalized avatar string (may not be a valid url).
*/
protected normalizeAvatarUrl(avatarUrl?: string): string {
if (!avatarUrl) {
return 'undefined';
}
if (avatarUrl.startsWith(`${this.site?.siteUrl}/theme/image.php`)) {
return 'default';
}
return avatarUrl;
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.obsProfileRefreshed?.off();
}
}

View File

@ -0,0 +1,44 @@
:host {
.core-user-profile-maininfo::part(native) {
flex-direction: column;
}
::ng-deep {
core-user-avatar {
display: block;
--core-avatar-size: var(--core-large-avatar-size);
height: calc(var(--core-avatar-size) + 16px);
img {
margin: 8px auto;
}
.contact-status {
width: 24px !important;
height: 24px !important;
right: calc(50% - 12px - var(--core-avatar-size) / 2) !important;
}
.edit-avatar {
position: absolute;
right: calc(50% - 15px - var(--core-avatar-size) / 2);
bottom: -12px;
:host-context([dir="rtl"]) & {
left: 0;
right: unset;
}
&::part(native) {
border-radius: 50%;
background: var(--ion-item-background);
}
}
}
}
}
:host-context([dir="rtl"]) ::ng-deep core-user-avatar .edit-avatar {
left: -24px;
right: unset;
}

View File

@ -14,20 +14,10 @@
<ion-list *ngIf="user && !isDeleted && isEnrolled">
<ion-item class="ion-text-center core-user-profile-maininfo">
<core-user-avatar [user]="user" [userId]="user.id" [linkProfile]="false" [checkOnline]="true">
<ion-button
class="edit-avatar"
*ngIf="canChangeProfilePicture"
(click)="changeProfilePicture()"
[attr.aria-label]="'core.user.newpicture' | translate"
fill="clear"
color="dark"
>
<ion-icon slot="icon-only" name="fas-pen" aria-hidden="true"></ion-icon>
</ion-button>
</core-user-avatar>
<ion-label>
<h2>{{ user.fullname }}</h2>
<p *ngIf="user.address">{{ user.address }}</p>
<p *ngIf="user.address"><ion-icon name="fas-map-marker-alt" [attr.aria-hidden]="true"></ion-icon> {{ user.address }}</p>
<p *ngIf="rolesFormatted" class="ion-text-wrap">
<strong>{{ 'core.user.roles' | translate}}</strong>{{'core.labelsep' | translate}}
{{ rolesFormatted }}

View File

@ -19,8 +19,6 @@ import { Subscription } from 'rxjs';
import { CoreSite } from '@classes/site';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import {
CoreUser,
@ -29,8 +27,6 @@ import {
} from '@features/user/services/user';
import { CoreUserHelper } from '@features/user/services/user-helper';
import { CoreUserDelegate, CoreUserDelegateService, CoreUserProfileHandlerData } from '@features/user/services/user-delegate';
import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuploader-helper';
import { CoreIonLoadingElement } from '@classes/ion-loading';
import { CoreUtils } from '@services/utils/utils';
import { CoreNavigator } from '@services/navigator';
import { CoreCourses } from '@features/courses/services/courses';
@ -54,7 +50,6 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
title?: string;
isDeleted = false;
isEnrolled = true;
canChangeProfilePicture = false;
rolesFormatted?: string;
actionHandlers: CoreUserProfileHandlerData[] = [];
newPageHandlers: CoreUserProfileHandlerData[] = [];
@ -72,7 +67,7 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
}
/**
* On init.
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
try {
@ -91,13 +86,6 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
this.courseId = undefined;
}
// Allow to change the profile image only in the app profile page.
this.canChangeProfilePicture =
!this.courseId &&
this.userId == this.site.getUserId() &&
this.site.canUploadFiles() &&
!CoreUser.isUpdatePictureDisabledInSite(this.site);
try {
await this.fetchUser();
@ -154,84 +142,12 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
this.isLoadingHandlers = !CoreUserDelegate.areHandlersLoaded(user.id);
});
await this.checkUserImageUpdated();
} catch (error) {
// Error is null for deleted users, do not show the modal.
CoreDomUtils.showErrorModal(error);
}
}
/**
* Check if current user image has changed.
*
* @return Promise resolved when done.
*/
protected async checkUserImageUpdated(): Promise<void> {
if (!this.site || !this.site.getInfo() || !this.user) {
return;
}
if (this.userId != this.site.getUserId() || !this.isUserAvatarDirty()) {
// Not current user or hasn't changed.
return;
}
// The current user image received is different than the one stored in site info. Assume the image was updated.
// Update the site info to get the right avatar in there.
try {
await CoreSites.updateSiteInfo(this.site.getId());
} catch {
// Cannot update site info. Assume the profile image is the right one.
CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, {
userId: this.userId,
picture: this.user.profileimageurl,
}, this.site.getId());
}
if (this.isUserAvatarDirty()) {
// The image is still different, this means that the good one is the one in site info.
await this.refreshUser();
} else {
// Now they're the same, send event to use the right avatar in the rest of the app.
CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, {
userId: this.userId,
picture: this.user.profileimageurl,
}, this.site.getId());
}
}
/**
* Opens dialog to change profile picture.
*/
async changeProfilePicture(): Promise<void> {
const maxSize = -1;
const title = Translate.instant('core.user.newpicture');
const mimetypes = CoreMimetypeUtils.getGroupMimeInfo('image', 'mimetypes');
let modal: CoreIonLoadingElement | undefined;
try {
const result = await CoreFileUploaderHelper.selectAndUploadFile(maxSize, title, mimetypes);
modal = await CoreDomUtils.showModalLoading('core.sending', true);
const profileImageURL = await CoreUser.changeProfilePicture(result.itemid, this.userId, this.site.getId());
CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, {
userId: this.userId,
picture: profileImageURL,
}, this.site.getId());
CoreSites.updateSiteInfo(this.site.getId());
this.refreshUser();
} catch (error) {
CoreDomUtils.showErrorModal(error);
} finally {
modal?.dismiss();
}
}
/**
* Refresh the user.
*
@ -285,48 +201,11 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
}
/**
* Page destroyed.
* @inheritdoc
*/
ngOnDestroy(): void {
this.subscription?.unsubscribe();
this.obsProfileRefreshed.off();
}
/**
* Check whether the user avatar is not up to date with site info.
*
* @return Whether the user avatar differs from site info cache.
*/
private isUserAvatarDirty(): boolean {
if (!this.user || !this.site) {
return false;
}
const courseAvatarUrl = this.normalizeAvatarUrl(this.user.profileimageurl);
const siteAvatarUrl = this.normalizeAvatarUrl(this.site.getInfo()?.userpictureurl);
return courseAvatarUrl !== siteAvatarUrl;
}
/**
* Normalize an avatar url regardless of theme.
*
* Given that the default image is the only one that can be changed per theme, any other url will stay the same. Note that
* the values returned by this function may not be valid urls, given that they are intended for string comparison.
*
* @param avatarUrl Avatar url.
* @return Normalized avatar string (may not be a valid url).
*/
private normalizeAvatarUrl(avatarUrl?: string): string {
if (!avatarUrl) {
return 'undefined';
}
if (avatarUrl.startsWith(`${this.site?.siteUrl}/theme/image.php`)) {
return 'default';
}
return avatarUrl;
}
}

View File

@ -18,21 +18,6 @@
height: 24px !important;
right: calc(50% - 12px - var(--core-avatar-size) / 2) !important;
}
.edit-avatar {
position: absolute;
right: calc(50% - 15px - var(--core-avatar-size) / 2);
bottom: -12px;
:host-context([dir="rtl"]) & {
left: 0;
right: unset;
}
&::part(native) {
border-radius: 50%;
background: var(--ion-item-background);
}
}
}
}