diff --git a/src/app/components/user-avatar/core-user-avatar.html b/src/app/components/user-avatar/core-user-avatar.html new file mode 100644 index 000000000..318573f43 --- /dev/null +++ b/src/app/components/user-avatar/core-user-avatar.html @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/app/components/user-avatar/user-avatar.scss b/src/app/components/user-avatar/user-avatar.scss new file mode 100644 index 000000000..679796d19 --- /dev/null +++ b/src/app/components/user-avatar/user-avatar.scss @@ -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; +} \ No newline at end of file diff --git a/src/app/components/user-avatar/user-avatar.ts b/src/app/components/user-avatar/user-avatar.ts new file mode 100644 index 000000000..95e634275 --- /dev/null +++ b/src/app/components/user-avatar/user-avatar.ts @@ -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: + */ +@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( + 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; +}; diff --git a/src/app/core/user/lang/en.json b/src/app/core/user/lang/en.json new file mode 100644 index 000000000..528fe4c5c --- /dev/null +++ b/src/app/core/user/lang/en.json @@ -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" +} \ No newline at end of file diff --git a/src/app/core/user/pages/profile/profile.html b/src/app/core/user/pages/profile/profile.html new file mode 100644 index 000000000..5f8b50497 --- /dev/null +++ b/src/app/core/user/pages/profile/profile.html @@ -0,0 +1,92 @@ + + + + + + {{ title }} + + + + + + + + + + + + + + + + +

{{ user.fullname }}

+

{{ user.address }}

+

+ {{ 'core.user.roles' | translate}}{{'core.labelsep' | translate}} + {{ rolesFormatted }} +

+
+
+ + + + + + +

{{handler.title | translate}}

+
+
+
+ + + + + +
+ + + + + + + + + +

{{ handler.title | translate }}

+
+
+ + + + + + {{ handler.title | translate }} + + + + +
+ + + + + +
+
diff --git a/src/app/core/user/pages/profile/profile.page.module.ts b/src/app/core/user/pages/profile/profile.page.module.ts new file mode 100644 index 000000000..67f81126b --- /dev/null +++ b/src/app/core/user/pages/profile/profile.page.module.ts @@ -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 {} diff --git a/src/app/core/user/pages/profile/profile.page.ts b/src/app/core/user/pages/profile/profile.page.ts new file mode 100644 index 000000000..96627a18b --- /dev/null +++ b/src/app/core/user/pages/profile/profile.page.ts @@ -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(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 { + 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 { + 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 { + 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(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(CoreUserProvider.PROFILE_PICTURE_UPDATED, { + userId: this.userId, + picture: this.user.profileimageurl, + }, this.site.getId()); + } + } + + /** + * Opens dialog to change profile picture. + */ + async changeProfilePicture(): Promise { + 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(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): Promise { + 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(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(); + } + +} diff --git a/src/app/core/user/pages/profile/profile.scss b/src/app/core/user/pages/profile/profile.scss new file mode 100644 index 000000000..454b80ab1 --- /dev/null +++ b/src/app/core/user/pages/profile/profile.scss @@ -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); + // } + // } +} \ No newline at end of file diff --git a/src/app/core/user/services/user.ts b/src/app/core/user/services/user.ts index cbfbe710c..4bec34d76 100644 --- a/src/app/core/user/services/user.ts +++ b/src/app/core/user/services/user.ts @@ -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. }; /** diff --git a/src/app/core/user/user-init.module.ts b/src/app/core/user/user-init.module.ts new file mode 100644 index 000000000..5a98299d0 --- /dev/null +++ b/src/app/core/user/user-init.module.ts @@ -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 {} diff --git a/src/app/core/user/user-routing.module.ts b/src/app/core/user/user-routing.module.ts new file mode 100644 index 000000000..0d117604f --- /dev/null +++ b/src/app/core/user/user-routing.module.ts @@ -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 {} diff --git a/src/app/core/user/user.module.ts b/src/app/core/user/user.module.ts new file mode 100644 index 000000000..2ea4d7f42 --- /dev/null +++ b/src/app/core/user/user.module.ts @@ -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 {} diff --git a/src/app/directives/user-link.ts b/src/app/directives/user-link.ts new file mode 100644 index 000000000..f731306a9 --- /dev/null +++ b/src/app/directives/user-link.ts @@ -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, + }), + }); + }); + } + +} diff --git a/src/core/components/components.module.ts b/src/core/components/components.module.ts index cfa381f7f..76c6ec45a 100644 --- a/src/core/components/components.module.ts +++ b/src/core/components/components.module.ts @@ -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 {} diff --git a/src/core/directives/directives.module.ts b/src/core/directives/directives.module.ts index 3be437b3c..06e4c6fa5 100644 --- a/src/core/directives/directives.module.ts +++ b/src/core/directives/directives.module.ts @@ -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 {} diff --git a/src/core/features/mainmenu/pages/more/more.html b/src/core/features/mainmenu/pages/more/more.html index 4776dc9b5..84dee2d54 100644 --- a/src/core/features/mainmenu/pages/more/more.html +++ b/src/core/features/mainmenu/pages/more/more.html @@ -9,7 +9,7 @@ - +

{{siteInfo.fullname}}

diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 247e2e209..e7ae51d23 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -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 { 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 = { diff --git a/src/theme/variables.scss b/src/theme/variables.scss index 29eebdf2c..836edd3fd 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -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;