MOBILE-3592 user: Implement profile page
parent
d733488963
commit
c608432d24
|
@ -0,0 +1,11 @@
|
|||
<img *ngIf="avatarUrl" [src]="avatarUrl" [alt]="'core.pictureof' | translate:{$a: fullname}" core-external-content
|
||||
onError="this.src='assets/img/user-avatar.png'" role="presentation" (click)="gotoProfile($event)">
|
||||
|
||||
<img *ngIf="!avatarUrl" src="assets/img/user-avatar.png" [alt]="'core.pictureof' | translate:{$a: fullname}" role="presentation"
|
||||
(click)="gotoProfile($event)">
|
||||
|
||||
<span *ngIf="checkOnline && isOnline()" class="contact-status online"></span>
|
||||
|
||||
<img *ngIf="extraIcon" [src]="extraIcon" alt="" role="presentation" class="core-avatar-extra-icon">
|
||||
|
||||
<ng-content></ng-content>
|
|
@ -0,0 +1,33 @@
|
|||
:host {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
.contact-status {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
&.online {
|
||||
border: 1px solid white;
|
||||
background-color: var(--core-online-color);
|
||||
}
|
||||
}
|
||||
|
||||
.core-avatar-extra-icon {
|
||||
margin: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
background: none;
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.toolbar) .contact-status {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { NavController } from '@ionic/angular';
|
||||
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreObject } from '@singletons/object';
|
||||
import { CoreUserProvider, CoreUserBasicData, CoreUserProfilePictureUpdatedData } from '@core/user/services/user';
|
||||
|
||||
/**
|
||||
* Component to display a "user avatar".
|
||||
*
|
||||
* Example: <ion-avatar core-user-avatar [user]="participant"></ion-avatar>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-user-avatar',
|
||||
templateUrl: 'core-user-avatar.html',
|
||||
styleUrls: ['user-avatar.scss'],
|
||||
})
|
||||
export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
@Input() user?: CoreUserWithAvatar;
|
||||
// The following params will override the ones in user object.
|
||||
@Input() profileUrl?: string;
|
||||
@Input() protected linkProfile = true; // Avoid linking to the profile if wanted.
|
||||
@Input() fullname?: string;
|
||||
@Input() protected userId?: number; // If provided or found it will be used to link the image to the profile.
|
||||
@Input() protected courseId?: number;
|
||||
@Input() checkOnline = false; // If want to check and show online status.
|
||||
@Input() extraIcon?: string; // Extra icon to show near the avatar.
|
||||
|
||||
avatarUrl?: string;
|
||||
|
||||
// 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.
|
||||
protected timetoshowusers = 300000; // Miliseconds default.
|
||||
protected currentUserId: number;
|
||||
protected pictureObserver: CoreEventObserver;
|
||||
|
||||
constructor(
|
||||
protected navCtrl: NavController,
|
||||
protected route: ActivatedRoute,
|
||||
) {
|
||||
this.currentUserId = CoreSites.instance.getCurrentSiteUserId();
|
||||
|
||||
this.pictureObserver = CoreEvents.on<CoreUserProfilePictureUpdatedData>(
|
||||
CoreUserProvider.PROFILE_PICTURE_UPDATED,
|
||||
(data) => {
|
||||
if (data.userId == this.userId) {
|
||||
this.avatarUrl = data.picture;
|
||||
}
|
||||
},
|
||||
CoreSites.instance.getCurrentSiteId(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.setFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to changes.
|
||||
*/
|
||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||
// If something change, update the fields.
|
||||
if (changes) {
|
||||
this.setFields();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fields from user.
|
||||
*/
|
||||
protected setFields(): void {
|
||||
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') {
|
||||
this.avatarUrl = profileUrl;
|
||||
}
|
||||
|
||||
this.fullname = this.fullname || (this.user && (this.user.fullname || this.user.userfullname));
|
||||
|
||||
this.userId = this.userId || (this.user && (this.user.userid || this.user.id));
|
||||
this.courseId = this.courseId || (this.user && this.user.courseid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for checking the time meets the 'online' condition.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
isOnline(): boolean {
|
||||
if (!this.user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CoreUtils.instance.isFalseOrZero(this.user.isonline)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.user.lastaccess) {
|
||||
// If the time has passed, don't show the online status.
|
||||
const time = new Date().getTime() - this.timetoshowusers;
|
||||
|
||||
return this.user.lastaccess * 1000 >= time;
|
||||
} else {
|
||||
// You have to have Internet access first.
|
||||
return !!this.user.isonline && CoreApp.instance.isOnline();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to user profile.
|
||||
*
|
||||
* @param event Click event.
|
||||
*/
|
||||
gotoProfile(event: Event): void {
|
||||
if (!this.linkProfile || !this.userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// @todo Decide which navCtrl to use. If this component is inside a split view, use the split view's master nav.
|
||||
this.navCtrl.navigateForward(['user'], {
|
||||
relativeTo: this.route,
|
||||
queryParams: CoreObject.removeUndefined({
|
||||
userId: this.userId,
|
||||
courseId: this.courseId,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.pictureObserver.off();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Type with all possible formats of user.
|
||||
*/
|
||||
type CoreUserWithAvatar = CoreUserBasicData & {
|
||||
userpictureurl?: string;
|
||||
userprofileimageurl?: string;
|
||||
profileimageurlsmall?: string;
|
||||
urls?: {
|
||||
profileimage?: string;
|
||||
};
|
||||
userfullname?: string;
|
||||
userid?: number;
|
||||
isonline?: boolean;
|
||||
courseid?: number;
|
||||
lastaccess?: number;
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"address": "Address",
|
||||
"city": "City/town",
|
||||
"contact": "Contact",
|
||||
"country": "Country",
|
||||
"description": "Description",
|
||||
"details": "Details",
|
||||
"detailsnotavailable": "The details of this user are not available to you.",
|
||||
"editingteacher": "Teacher",
|
||||
"email": "Email address",
|
||||
"emailagain": "Email (again)",
|
||||
"errorloaduser": "Error loading user.",
|
||||
"firstname": "First name",
|
||||
"interests": "Interests",
|
||||
"lastname": "Surname",
|
||||
"manager": "Manager",
|
||||
"newpicture": "New picture",
|
||||
"noparticipants": "No participants found for this course",
|
||||
"participants": "Participants",
|
||||
"phone1": "Phone",
|
||||
"phone2": "Mobile phone",
|
||||
"roles": "Roles",
|
||||
"sendemail": "Email",
|
||||
"student": "Student",
|
||||
"teacher": "Non-editing teacher",
|
||||
"webpage": "Web page"
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title *ngIf="title">{{ title }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!userLoaded" (ionRefresh)="refreshUser($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="userLoaded">
|
||||
<ion-list *ngIf="user && !isDeleted && isEnrolled">
|
||||
<ion-item class="ion-text-center item-avatar-center">
|
||||
<ion-avatar>
|
||||
<core-user-avatar [user]="user" [userId]="user.id" [linkProfile]="false"
|
||||
[checkOnline]="true">
|
||||
<ion-icon *ngIf="canChangeProfilePicture" name="fa-pen" class="core-icon-foreground"
|
||||
(click)="changeProfilePicture()">
|
||||
</ion-icon>
|
||||
</core-user-avatar>
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<h2>{{ user.fullname }}</h2>
|
||||
<p *ngIf="user.address">{{ user.address }}</p>
|
||||
<p *ngIf="rolesFormatted" class="ion-text-wrap">
|
||||
<strong>{{ 'core.user.roles' | translate}}</strong>{{'core.labelsep' | translate}}
|
||||
{{ rolesFormatted }}
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-grid class="core-user-communication-handlers"
|
||||
*ngIf="(communicationHandlers && communicationHandlers.length) || isLoadingHandlers">
|
||||
<ion-row class="ion-no-padding justify-content-between"
|
||||
*ngIf="communicationHandlers && communicationHandlers.length">
|
||||
<ion-col *ngFor="let handler of communicationHandlers" class="ion-align-self-center ion-text-center">
|
||||
<a (click)="handlerClicked($event, handler)" [ngClass]="['core-user-profile-handler', handler.class]"
|
||||
title="{{handler.title | translate}}">
|
||||
<ion-icon [name]="handler.icon" slot="start"></ion-icon>
|
||||
<p>{{handler.title | translate}}</p>
|
||||
</a>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-row class="ion-no-padding">
|
||||
<ion-col class="ion-text-center core-loading-handlers" *ngIf="isLoadingHandlers">
|
||||
<ion-spinner></ion-spinner>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
<ion-item button class="ion-text-wrap core-user-profile-handler" (click)="openUserDetails()"
|
||||
title="{{ 'core.user.details' | translate }}">
|
||||
<ion-icon name="fa-user" slot="start"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.user.details' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-center core-loading-handlers" *ngIf="isLoadingHandlers">
|
||||
<ion-spinner></ion-spinner>
|
||||
</ion-item>
|
||||
|
||||
<ion-item button *ngFor="let handler of newPageHandlers" class="ion-text-wrap" (click)="handlerClicked($event, handler)"
|
||||
[ngClass]="['core-user-profile-handler', handler.class]" [hidden]="handler.hidden"
|
||||
title="{{ handler.title | translate }}">
|
||||
<ion-label>
|
||||
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start"></ion-icon>
|
||||
<h2>{{ handler.title | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item *ngIf="actionHandlers && actionHandlers.length">
|
||||
<ion-button *ngFor="let handler of actionHandlers" expand="block" fill="outline"
|
||||
[ngClass]="['core-user-profile-handler', handler.class]" (click)="handlerClicked($event, handler)"
|
||||
[hidden]="handler.hidden" title="{{ handler.title | translate }}" [disabled]="handler.spinner">
|
||||
<ion-label>
|
||||
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start"></ion-icon>
|
||||
<span>{{ handler.title | translate }}</span>
|
||||
</ion-label>
|
||||
<ion-spinner *ngIf="handler.spinner"></ion-spinner>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<core-empty-box *ngIf="!user && !isDeleted && isEnrolled" icon="fa-user"
|
||||
[message]=" 'core.user.detailsnotavailable' | translate">
|
||||
</core-empty-box>
|
||||
<core-empty-box *ngIf="isDeleted" icon="fa-user" [message]="'core.userdeleted' | translate"></core-empty-box>
|
||||
<core-empty-box *ngIf="!isEnrolled" icon="fa-user" [message]="'core.notenrolledprofile' | translate"></core-empty-box>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,47 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
|
||||
import { CoreUserProfilePage } from './profile.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CoreUserProfilePage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreUserProfilePage,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class CoreUserProfilePageModule {}
|
|
@ -0,0 +1,289 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { IonRefresher, NavController } from '@ionic/angular';
|
||||
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/core.singletons';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import {
|
||||
CoreUser,
|
||||
CoreUserProfile,
|
||||
CoreUserProfilePictureUpdatedData,
|
||||
CoreUserProfileRefreshedData,
|
||||
CoreUserProvider,
|
||||
} from '@core/user/services/user';
|
||||
import { CoreUserHelper } from '@core/user/services/user.helper';
|
||||
import { CoreUserDelegate, CoreUserProfileHandlerData } from '@core/user/services/user.delegate';
|
||||
import { CoreFileUploaderHelper } from '@core/fileuploader/services/fileuploader.helper';
|
||||
import { CoreIonLoadingElement } from '@classes/ion-loading';
|
||||
import { CoreUtils } from '@/app/services/utils/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'page-core-user-profile',
|
||||
templateUrl: 'profile.html',
|
||||
styleUrls: ['profile.scss'],
|
||||
})
|
||||
export class CoreUserProfilePage implements OnInit, OnDestroy {
|
||||
|
||||
protected courseId!: number;
|
||||
protected userId!: number;
|
||||
protected site?: CoreSite;
|
||||
protected obsProfileRefreshed: CoreEventObserver;
|
||||
protected subscription?: Subscription;
|
||||
|
||||
userLoaded = false;
|
||||
isLoadingHandlers = false;
|
||||
user?: CoreUserProfile;
|
||||
title?: string;
|
||||
isDeleted = false;
|
||||
isEnrolled = true;
|
||||
canChangeProfilePicture = false;
|
||||
rolesFormatted?: string;
|
||||
actionHandlers: CoreUserProfileHandlerData[] = [];
|
||||
newPageHandlers: CoreUserProfileHandlerData[] = [];
|
||||
communicationHandlers: CoreUserProfileHandlerData[] = [];
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
protected navCtrl: NavController,
|
||||
protected userDelegate: CoreUserDelegate,
|
||||
) {
|
||||
|
||||
this.obsProfileRefreshed = CoreEvents.on<CoreUserProfileRefreshedData>(CoreUserProvider.PROFILE_REFRESHED, (data) => {
|
||||
if (!this.user || !data.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.user.email = data.user.email;
|
||||
this.user.address = CoreUserHelper.instance.formatAddress('', data.user.city, data.user.country);
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
}
|
||||
|
||||
/**
|
||||
* On init.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.site = CoreSites.instance.getCurrentSite();
|
||||
this.userId = this.route.snapshot.queryParams['userId'];
|
||||
this.courseId = this.route.snapshot.queryParams['courseId'];
|
||||
|
||||
if (!this.site) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow to change the profile image only in the app profile page.
|
||||
this.canChangeProfilePicture =
|
||||
(!this.courseId || this.courseId == this.site.getSiteHomeId()) &&
|
||||
this.userId == this.site.getUserId() &&
|
||||
this.site.canUploadFiles() &&
|
||||
CoreUser.instance.canUpdatePictureInSite(this.site) &&
|
||||
!CoreUser.instance.isUpdatePictureDisabledInSite(this.site);
|
||||
|
||||
try {
|
||||
await this.fetchUser();
|
||||
|
||||
try {
|
||||
await CoreUser.instance.logView(this.userId, this.courseId, this.user!.fullname);
|
||||
} catch (error) {
|
||||
this.isDeleted = error?.errorcode === 'userdeleted';
|
||||
this.isEnrolled = error?.errorcode !== 'notenrolledprofile';
|
||||
}
|
||||
} finally {
|
||||
this.userLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the user and updates the view.
|
||||
*/
|
||||
async fetchUser(): Promise<void> {
|
||||
try {
|
||||
const user = await CoreUser.instance.getProfile(this.userId, this.courseId);
|
||||
|
||||
user.address = CoreUserHelper.instance.formatAddress('', user.city, user.country);
|
||||
this.rolesFormatted = 'roles' in user ? CoreUserHelper.instance.formatRoleList(user.roles) : '';
|
||||
|
||||
this.user = user;
|
||||
this.title = user.fullname;
|
||||
|
||||
// If there's already a subscription, unsubscribe because we'll get a new one.
|
||||
this.subscription?.unsubscribe();
|
||||
|
||||
this.subscription = this.userDelegate.getProfileHandlersFor(user, this.courseId).subscribe((handlers) => {
|
||||
this.actionHandlers = [];
|
||||
this.newPageHandlers = [];
|
||||
this.communicationHandlers = [];
|
||||
handlers.forEach((handler) => {
|
||||
switch (handler.type) {
|
||||
case CoreUserDelegate.TYPE_COMMUNICATION:
|
||||
this.communicationHandlers.push(handler.data);
|
||||
break;
|
||||
case CoreUserDelegate.TYPE_ACTION:
|
||||
this.actionHandlers.push(handler.data);
|
||||
break;
|
||||
case CoreUserDelegate.TYPE_NEW_PAGE:
|
||||
default:
|
||||
this.newPageHandlers.push(handler.data);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this.isLoadingHandlers = !this.userDelegate.areHandlersLoaded(user.id);
|
||||
});
|
||||
|
||||
await this.checkUserImageUpdated();
|
||||
|
||||
} catch (error) {
|
||||
// Error is null for deleted users, do not show the modal.
|
||||
CoreDomUtils.instance.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.user.profileimageurl == this.site.getInfo()!.userpictureurl) {
|
||||
// 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.instance.updateSiteInfo(this.site.getId());
|
||||
} catch {
|
||||
// Cannot update site info. Assume the profile image is the right one.
|
||||
CoreEvents.trigger<CoreUserProfilePictureUpdatedData>(CoreUserProvider.PROFILE_PICTURE_UPDATED, {
|
||||
userId: this.userId,
|
||||
picture: this.user.profileimageurl,
|
||||
}, this.site.getId());
|
||||
}
|
||||
|
||||
if (this.user.profileimageurl != this.site.getInfo()!.userpictureurl) {
|
||||
// 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<CoreUserProfilePictureUpdatedData>(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.instance.instant('core.user.newpicture');
|
||||
const mimetypes = CoreMimetypeUtils.instance.getGroupMimeInfo('image', 'mimetypes');
|
||||
let modal: CoreIonLoadingElement | undefined;
|
||||
|
||||
try {
|
||||
const result = await CoreFileUploaderHelper.instance.selectAndUploadFile(maxSize, title, mimetypes);
|
||||
|
||||
modal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
|
||||
|
||||
const profileImageURL = await CoreUser.instance.changeProfilePicture(result.itemid, this.userId, this.site!.getId());
|
||||
|
||||
CoreEvents.trigger<CoreUserProfilePictureUpdatedData>(CoreUserProvider.PROFILE_PICTURE_UPDATED, {
|
||||
userId: this.userId,
|
||||
picture: profileImageURL,
|
||||
}, this.site!.getId());
|
||||
|
||||
CoreSites.instance.updateSiteInfo(this.site!.getId());
|
||||
|
||||
this.refreshUser();
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModal(error);
|
||||
} finally {
|
||||
modal?.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the user.
|
||||
*
|
||||
* @param event Event.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async refreshUser(event?: CustomEvent<IonRefresher>): Promise<void> {
|
||||
await CoreUtils.instance.ignoreErrors(Promise.all([
|
||||
CoreUser.instance.invalidateUserCache(this.userId),
|
||||
// @todo this.coursesProvider.invalidateUserNavigationOptions(),
|
||||
// this.coursesProvider.invalidateUserAdministrationOptions()
|
||||
]));
|
||||
|
||||
await this.fetchUser();
|
||||
|
||||
event?.detail.complete();
|
||||
|
||||
if (this.user) {
|
||||
CoreEvents.trigger<CoreUserProfileRefreshedData>(CoreUserProvider.PROFILE_REFRESHED, {
|
||||
courseId: this.courseId,
|
||||
userId: this.userId,
|
||||
user: this.user,
|
||||
}, this.site?.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the page with the user details.
|
||||
*/
|
||||
openUserDetails(): void {
|
||||
// @todo: Navigate out of split view if this page is in the right pane.
|
||||
this.navCtrl.navigateForward(['../about'], {
|
||||
relativeTo: this.route,
|
||||
queryParams: {
|
||||
courseId: this.courseId,
|
||||
userId: this.userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A handler was clicked.
|
||||
*
|
||||
* @param event Click event.
|
||||
* @param handler Handler that was clicked.
|
||||
*/
|
||||
handlerClicked(event: Event, handler: CoreUserProfileHandlerData): void {
|
||||
// @todo: Pass the right navCtrl if this page is in the right pane of split view.
|
||||
handler.action(event, this.navCtrl, this.user!, this.courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.subscription?.unsubscribe();
|
||||
this.obsProfileRefreshed.off();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
:host {
|
||||
// @todo
|
||||
// .core-icon-foreground {
|
||||
// position: absolute;
|
||||
// @include position(null, 0, 0, null);
|
||||
// font-size: 24px;
|
||||
// line-height: 30px;
|
||||
// text-align: center;
|
||||
|
||||
// width: 30px;
|
||||
// height: 30px;
|
||||
// border-radius: 50%;
|
||||
// background-color: $white;
|
||||
// @include darkmode() {
|
||||
// background: $core-dark-item-bg-color;
|
||||
// }
|
||||
// }
|
||||
// [core-user-avatar].item-avatar-center {
|
||||
// display: inline-block;
|
||||
// img {
|
||||
// margin: 0;
|
||||
// }
|
||||
// .contact-status {
|
||||
// width: 24px;
|
||||
// height: 24px;
|
||||
// }
|
||||
// }
|
||||
|
||||
// .core-user-communication-handlers {
|
||||
// background: $list-background-color;
|
||||
// border-bottom: 1px solid $list-border-color;
|
||||
|
||||
// @include darkmode() {
|
||||
// background: $core-dark-item-bg-color;
|
||||
// }
|
||||
|
||||
// .core-user-profile-handler {
|
||||
// background: $list-background-color;
|
||||
// border: 0;
|
||||
// color: $core-user-profile-communication-icons-color;
|
||||
|
||||
// @include darkmode() {
|
||||
// background: $core-dark-item-bg-color;
|
||||
// }
|
||||
|
||||
// p {
|
||||
// margin: 0;
|
||||
// }
|
||||
|
||||
// .icon {
|
||||
// border-radius: 50%;
|
||||
// width: 32px;
|
||||
// height: 32px;
|
||||
// max-width: 32px;
|
||||
// font-size: 22px;
|
||||
// line-height: 32px;
|
||||
// color: white;
|
||||
// background-color: $core-user-profile-communication-icons-color;
|
||||
// margin-bottom: 5px;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// .core-user-profile-handler {
|
||||
// ion-spinner {
|
||||
// @include margin(null, null, null, 0.3em);
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -810,7 +810,8 @@ export class CoreUser extends makeSingleton(CoreUserProvider) {}
|
|||
*/
|
||||
export type CoreUserProfileRefreshedData = {
|
||||
courseId: number; // Course the user profile belongs to.
|
||||
user: CoreUserProfile; // User affected.
|
||||
userId: number; // User ID.
|
||||
user?: CoreUserProfile; // User affected.
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { CoreMainMenuRoutingModule } from '@core/mainmenu/mainmenu-routing.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'user',
|
||||
loadChildren: () => import('@core/user/user.module').then(m => m.CoreUserModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreMainMenuRoutingModule.forChild(routes),
|
||||
],
|
||||
exports: [
|
||||
CoreMainMenuRoutingModule,
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
})
|
||||
export class CoreUserInitModule {}
|
|
@ -0,0 +1,34 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'profile',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
loadChildren: () => import('./pages/profile/profile.page.module').then( m => m.CoreUserProfilePageModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class CoreUserRoutingModule {}
|
|
@ -0,0 +1,24 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { CoreUserRoutingModule } from './user-routing.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreUserRoutingModule,
|
||||
],
|
||||
})
|
||||
export class CoreUserModule {}
|
|
@ -0,0 +1,66 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Directive, Input, OnInit, ElementRef } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { NavController } from '@ionic/angular';
|
||||
|
||||
import { CoreObject } from '@singletons/object';
|
||||
|
||||
/**
|
||||
* Directive to go to user profile on click.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[core-user-link]',
|
||||
})
|
||||
export class CoreUserLinkDirective implements OnInit {
|
||||
|
||||
@Input() userId?: number; // User id to open the profile.
|
||||
@Input() courseId?: number; // If set, course id to show the user info related to that course.
|
||||
|
||||
protected element: HTMLElement;
|
||||
|
||||
constructor(
|
||||
element: ElementRef,
|
||||
protected navCtrl: NavController,
|
||||
protected route: ActivatedRoute,
|
||||
) {
|
||||
this.element = element.nativeElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function executed when the component is initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.element.addEventListener('click', (event) => {
|
||||
// If the event prevented default action, do nothing.
|
||||
if (event.defaultPrevented || !this.userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// @todo If this directive is inside a split view, use the split view's master nav.
|
||||
this.navCtrl.navigateForward(['user'], {
|
||||
relativeTo: this.route,
|
||||
queryParams: CoreObject.removeUndefined({
|
||||
userId: this.userId,
|
||||
courseId: this.courseId,
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -35,6 +35,7 @@ import { CoreProgressBarComponent } from './progress-bar/progress-bar';
|
|||
import { CoreContextMenuComponent } from './context-menu/context-menu';
|
||||
import { CoreContextMenuItemComponent } from './context-menu/context-menu-item';
|
||||
import { CoreContextMenuPopoverComponent } from './context-menu/context-menu-popover';
|
||||
import { CoreUserAvatarComponent } from './user-avatar/user-avatar';
|
||||
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CorePipesModule } from '@pipes/pipes.module';
|
||||
|
@ -61,6 +62,7 @@ import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
|||
CoreContextMenuItemComponent,
|
||||
CoreContextMenuPopoverComponent,
|
||||
CoreNavBarButtonsComponent,
|
||||
CoreUserAvatarComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -89,6 +91,7 @@ import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
|||
CoreContextMenuItemComponent,
|
||||
CoreContextMenuPopoverComponent,
|
||||
CoreNavBarButtonsComponent,
|
||||
CoreUserAvatarComponent,
|
||||
],
|
||||
})
|
||||
export class CoreComponentsModule {}
|
||||
|
|
|
@ -22,6 +22,7 @@ import { CoreLinkDirective } from './link';
|
|||
import { CoreLongPressDirective } from './long-press';
|
||||
import { CoreSupressEventsDirective } from './supress-events';
|
||||
import { CoreFaIconDirective } from './fa-icon';
|
||||
import { CoreUserLinkDirective } from './user-link';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -33,6 +34,7 @@ import { CoreFaIconDirective } from './fa-icon';
|
|||
CoreSupressEventsDirective,
|
||||
CoreFabDirective,
|
||||
CoreFaIconDirective,
|
||||
CoreUserLinkDirective,
|
||||
],
|
||||
imports: [],
|
||||
exports: [
|
||||
|
@ -44,6 +46,7 @@ import { CoreFaIconDirective } from './fa-icon';
|
|||
CoreSupressEventsDirective,
|
||||
CoreFabDirective,
|
||||
CoreFaIconDirective,
|
||||
CoreUserLinkDirective,
|
||||
],
|
||||
})
|
||||
export class CoreDirectivesModule {}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-item *ngIf="siteInfo" class="ion-text-wrap"> <!-- @todo core-user-link [userId]="siteInfo.userid" -->
|
||||
<ion-item *ngIf="siteInfo" class="ion-text-wrap" core-user-link [userId]="siteInfo.userid">
|
||||
<ion-avatar slot="start"></ion-avatar> <!-- @todo core-user-avatar [user]="siteInfo" -->
|
||||
<ion-label>
|
||||
<h2>{{siteInfo.fullname}}</h2>
|
||||
|
|
|
@ -656,6 +656,10 @@ export class CoreDomUtilsProvider {
|
|||
* @return Error message, null if no error should be displayed.
|
||||
*/
|
||||
getErrorMessage(error: CoreError | CoreTextErrorObject | string, needsTranslate?: boolean): string | null {
|
||||
if (typeof error != 'string' && !error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let extraInfo = '';
|
||||
let errorMessage: string | undefined;
|
||||
|
||||
|
@ -1332,21 +1336,21 @@ export class CoreDomUtilsProvider {
|
|||
* @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed.
|
||||
* @return Promise resolved with the alert modal.
|
||||
*/
|
||||
showErrorModal(
|
||||
async showErrorModal(
|
||||
error: CoreError | CoreTextErrorObject | string,
|
||||
needsTranslate?: boolean,
|
||||
autocloseTime?: number,
|
||||
): Promise<HTMLIonAlertElement | null> {
|
||||
if (this.isCanceledError(error)) {
|
||||
// It's a canceled error, don't display an error.
|
||||
return Promise.resolve(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = this.getErrorMessage(error, needsTranslate);
|
||||
|
||||
if (message === null) {
|
||||
// Message doesn't need to be displayed, stop.
|
||||
return Promise.resolve(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
const alertOptions: AlertOptions = {
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
--purple: var(--custom-purple, #8e24aa); // Accent (never text).
|
||||
|
||||
--core-color: var(--custom-main-color, var(--orange));
|
||||
--core-online-color: #5cb85c;
|
||||
|
||||
--ion-color-primary: var(--core-color);
|
||||
--ion-color-primary-rgb: 249,128,18;
|
||||
|
|
Loading…
Reference in New Issue