MOBILE-3807 user: Edit avatar only from details
parent
434a2a90f2
commit
0bbfb898d1
|
@ -18,7 +18,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@if ($core-user-hide-siteinfo) {
|
||||
.core-usermenu-siteinfo {
|
||||
display: none;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 }}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue