MOBILE-3592 user: Implement profile page

main
Dani Palou 2020-11-13 13:43:35 +01:00
parent d733488963
commit c608432d24
18 changed files with 925 additions and 5 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
};
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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