diff --git a/scripts/langindex.json b/scripts/langindex.json index 15dc8e894..b269f6b62 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1798,6 +1798,8 @@ "core.loading": "moodle", "core.loadmore": "local_moodlemobileapp", "core.location": "moodle", + "core.login.accounts": "admin", + "core.login.add": "moodle", "core.login.auth_email": "auth_email/pluginname", "core.login.authenticating": "local_moodlemobileapp", "core.login.cancel": "moodle", @@ -1894,6 +1896,7 @@ "core.login.reconnect": "local_moodlemobileapp", "core.login.reconnectdescription": "local_moodlemobileapp", "core.login.reconnectssodescription": "local_moodlemobileapp", + "core.login.removeaccount": "local_moodlemobileapp", "core.login.resendemail": "moodle", "core.login.searchby": "local_moodlemobileapp", "core.login.security_question": "auth", @@ -1913,6 +1916,7 @@ "core.login.startsignup": "moodle", "core.login.stillcantconnect": "local_moodlemobileapp", "core.login.supplyinfo": "moodle", + "core.login.toggleremove": "local_moodlemobileapp", "core.login.username": "moodle", "core.login.usernameoremail": "moodle", "core.login.usernamerequired": "local_moodlemobileapp", @@ -1922,11 +1926,9 @@ "core.login.youcanstillconnectwithcredentials": "local_moodlemobileapp", "core.login.yourenteredsite": "local_moodlemobileapp", "core.lostconnection": "local_moodlemobileapp", - "core.mainmenu.changesite": "local_moodlemobileapp", - "core.mainmenu.help": "moodle", "core.mainmenu.home": "moodle", "core.mainmenu.logout": "moodle", - "core.mainmenu.website": "local_moodlemobileapp", + "core.mainmenu.switchaccount": "local_moodlemobileapp", "core.maxfilesize": "moodle", "core.maxsizeandattachments": "moodle", "core.min": "moodle", @@ -1960,7 +1962,7 @@ "core.mod_wiki": "wiki/pluginname", "core.mod_workshop": "workshop/pluginname", "core.moduleintro": "moodle", - "core.more": "moodle", + "core.more": "moodle/moremenu", "core.mygroups": "group", "core.name": "moodle", "core.needhelp": "local_moodlemobileapp", @@ -2232,6 +2234,7 @@ "core.updaterequireddesc": "local_moodlemobileapp", "core.upgraderunning": "error", "core.user": "moodle", + "core.user.account": "local_moodlemobileapp", "core.user.address": "moodle", "core.user.city": "moodle", "core.user.contact": "local_moodlemobileapp", @@ -2252,6 +2255,7 @@ "core.user.participants": "moodle", "core.user.phone1": "moodle", "core.user.phone2": "moodle", + "core.user.profile": "moodle", "core.user.roles": "moodle", "core.user.sendemail": "local_moodlemobileapp", "core.user.student": "moodle/defaultcoursestudent", diff --git a/src/addons/block/privatefiles/services/block-handler.ts b/src/addons/block/privatefiles/services/block-handler.ts index 73c160eaf..bf4c9aaf7 100644 --- a/src/addons/block/privatefiles/services/block-handler.ts +++ b/src/addons/block/privatefiles/services/block-handler.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreBlockHandlerData } from '@features/block/services/block-delegate'; import { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block'; import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; -import { AddonPrivateFilesMainMenuHandlerService } from '@/addons/privatefiles/services/handlers/mainmenu'; +import { AddonPrivateFilesUserHandlerService } from '@addons/privatefiles/services/handlers/user'; import { makeSingleton } from '@singletons'; /** @@ -39,7 +39,7 @@ export class AddonBlockPrivateFilesHandlerService extends CoreBlockBaseHandler { title: 'addon.block_privatefiles.pluginname', class: 'addon-block-private-files', component: CoreBlockOnlyTitleComponent, - link: AddonPrivateFilesMainMenuHandlerService.PAGE_NAME, + link: AddonPrivateFilesUserHandlerService.PAGE_NAME, linkParams: { root: 'my' }, navOptions: { preferCurrentTab: false, diff --git a/src/addons/blog/blog-lazy.module.ts b/src/addons/blog/blog-lazy.module.ts index 5fab11220..2bd801405 100644 --- a/src/addons/blog/blog-lazy.module.ts +++ b/src/addons/blog/blog-lazy.module.ts @@ -22,6 +22,7 @@ import { CoreCommentsComponentsModule } from '@features/comments/components/comp import { CoreTagComponentsModule } from '@features/tag/components/components.module'; import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; import { AddonBlogMainMenuHandlerService } from './services/handlers/mainmenu'; +import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module'; function buildRoutes(injector: Injector): Routes { return [ @@ -39,6 +40,7 @@ function buildRoutes(injector: Injector): Routes { CoreSharedModule, CoreCommentsComponentsModule, CoreTagComponentsModule, + CoreMainMenuComponentsModule, ], exports: [RouterModule], providers: [ diff --git a/src/addons/blog/pages/entries/entries.html b/src/addons/blog/pages/entries/entries.html index 6dd13b87c..60d027fcb 100644 --- a/src/addons/blog/pages/entries/entries.html +++ b/src/addons/blog/pages/entries/entries.html @@ -4,7 +4,9 @@

{{ title | translate }}

- + + + diff --git a/src/addons/calendar/pages/index/index.html b/src/addons/calendar/pages/index/index.html index 1533cc434..b67f85700 100644 --- a/src/addons/calendar/pages/index/index.html +++ b/src/addons/calendar/pages/index/index.html @@ -22,6 +22,7 @@ [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"> + diff --git a/src/addons/calendar/pages/index/index.module.ts b/src/addons/calendar/pages/index/index.module.ts index be80e63e6..0ecbc7b6e 100644 --- a/src/addons/calendar/pages/index/index.module.ts +++ b/src/addons/calendar/pages/index/index.module.ts @@ -19,6 +19,7 @@ import { CoreSharedModule } from '@/core/shared.module'; import { AddonCalendarComponentsModule } from '../../components/components.module'; import { AddonCalendarIndexPage } from './index.page'; +import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module'; const routes: Routes = [ { @@ -32,6 +33,7 @@ const routes: Routes = [ RouterModule.forChild(routes), CoreSharedModule, AddonCalendarComponentsModule, + CoreMainMenuComponentsModule, ], declarations: [ AddonCalendarIndexPage, diff --git a/src/addons/messages/pages/discussions-35/discussions.html b/src/addons/messages/pages/discussions-35/discussions.html index 5c0b2b5e3..8824fe578 100644 --- a/src/addons/messages/pages/discussions-35/discussions.html +++ b/src/addons/messages/pages/discussions-35/discussions.html @@ -5,6 +5,7 @@

{{ 'addon.messages.messages' | translate }}

+ diff --git a/src/addons/messages/pages/discussions-35/discussions.module.ts b/src/addons/messages/pages/discussions-35/discussions.module.ts index 15ec2604e..f26263d62 100644 --- a/src/addons/messages/pages/discussions-35/discussions.module.ts +++ b/src/addons/messages/pages/discussions-35/discussions.module.ts @@ -23,6 +23,7 @@ import { CoreSearchComponentsModule } from '@features/search/components/componen import { AddonMessagesDiscussions35Page } from './discussions.page'; import { AddonMessagesMainMenuHandlerService } from '@addons/messages/services/handlers/mainmenu'; +import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module'; const mobileRoutes: Routes = [ { @@ -58,6 +59,7 @@ const routes: Routes = [ RouterModule.forChild(routes), CoreSharedModule, CoreSearchComponentsModule, + CoreMainMenuComponentsModule, ], declarations: [ AddonMessagesDiscussions35Page, diff --git a/src/addons/messages/pages/group-conversations/group-conversations.html b/src/addons/messages/pages/group-conversations/group-conversations.html index 7d02aa119..d55475d6c 100644 --- a/src/addons/messages/pages/group-conversations/group-conversations.html +++ b/src/addons/messages/pages/group-conversations/group-conversations.html @@ -11,6 +11,7 @@ + diff --git a/src/addons/messages/pages/group-conversations/group-conversations.module.ts b/src/addons/messages/pages/group-conversations/group-conversations.module.ts index a4390583e..8fbcc9bae 100644 --- a/src/addons/messages/pages/group-conversations/group-conversations.module.ts +++ b/src/addons/messages/pages/group-conversations/group-conversations.module.ts @@ -22,6 +22,7 @@ import { CoreSharedModule } from '@/core/shared.module'; import { AddonMessagesGroupConversationsPage } from './group-conversations.page'; import { AddonMessagesMainMenuHandlerService } from '@addons/messages/services/handlers/mainmenu'; +import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module'; const mobileRoutes: Routes = [ { @@ -56,6 +57,7 @@ const routes: Routes = [ imports: [ RouterModule.forChild(routes), CoreSharedModule, + CoreMainMenuComponentsModule, ], declarations: [ AddonMessagesGroupConversationsPage, diff --git a/src/addons/notifications/pages/list/list.html b/src/addons/notifications/pages/list/list.html index 3aba09f1e..a500d7296 100644 --- a/src/addons/notifications/pages/list/list.html +++ b/src/addons/notifications/pages/list/list.html @@ -4,6 +4,9 @@

{{ 'addon.notifications.notifications' | translate }}

+ + + diff --git a/src/addons/notifications/pages/list/list.module.ts b/src/addons/notifications/pages/list/list.module.ts index 5a4f6144d..b592079e7 100644 --- a/src/addons/notifications/pages/list/list.module.ts +++ b/src/addons/notifications/pages/list/list.module.ts @@ -18,6 +18,7 @@ import { RouterModule, Routes } from '@angular/router'; import { CoreSharedModule } from '@/core/shared.module'; import { AddonNotificationsComponentsModule } from '../../components/components.module'; import { AddonNotificationsListPage } from './list'; +import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module'; const routes: Routes = [ { @@ -31,6 +32,7 @@ const routes: Routes = [ RouterModule.forChild(routes), CoreSharedModule, AddonNotificationsComponentsModule, + CoreMainMenuComponentsModule, ], declarations: [ AddonNotificationsListPage, diff --git a/src/addons/privatefiles/privatefiles-lazy.module.ts b/src/addons/privatefiles/privatefiles-lazy.module.ts index fa78f9da5..5791c5dc0 100644 --- a/src/addons/privatefiles/privatefiles-lazy.module.ts +++ b/src/addons/privatefiles/privatefiles-lazy.module.ts @@ -16,14 +16,14 @@ import { Injector, NgModule } from '@angular/core'; import { RouterModule, ROUTES, Routes } from '@angular/router'; import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; -import { AddonPrivateFilesMainMenuHandlerService } from './services/handlers/mainmenu'; +import { AddonPrivateFilesUserHandlerService } from './services/handlers/user'; function buildRoutes(injector: Injector): Routes { return [ { path: 'root', data: { - mainMenuTabRoot: AddonPrivateFilesMainMenuHandlerService.PAGE_NAME, + mainMenuTabRoot: AddonPrivateFilesUserHandlerService.PAGE_NAME, }, loadChildren: () => import('./pages/index/index.module').then(m => m.AddonPrivateFilesIndexPageModule), }, diff --git a/src/addons/privatefiles/privatefiles.module.ts b/src/addons/privatefiles/privatefiles.module.ts index ca91c9d3f..635a80afb 100644 --- a/src/addons/privatefiles/privatefiles.module.ts +++ b/src/addons/privatefiles/privatefiles.module.ts @@ -15,12 +15,12 @@ import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; -import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate'; import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module'; -import { AddonPrivateFilesMainMenuHandler, AddonPrivateFilesMainMenuHandlerService } from './services/handlers/mainmenu'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; import { AddonPrivateFilesProvider } from './services/privatefiles'; import { AddonPrivateFilesHelperProvider } from './services/privatefiles-helper'; +import { CoreUserDelegate } from '@features/user/services/user-delegate'; +import { AddonPrivateFilesUserHandler, AddonPrivateFilesUserHandlerService } from './services/handlers/user'; export const ADDON_PRIVATEFILES_SERVICES: Type[] = [ AddonPrivateFilesProvider, @@ -29,7 +29,7 @@ export const ADDON_PRIVATEFILES_SERVICES: Type[] = [ const routes: Routes = [ { - path: AddonPrivateFilesMainMenuHandlerService.PAGE_NAME, + path: AddonPrivateFilesUserHandlerService.PAGE_NAME, loadChildren: () => import('@/addons/privatefiles/privatefiles-lazy.module').then(m => m.AddonPrivateFilesLazyModule), }, ]; @@ -45,7 +45,7 @@ const routes: Routes = [ provide: APP_INITIALIZER, multi: true, useValue: () => { - CoreMainMenuDelegate.registerHandler(AddonPrivateFilesMainMenuHandler.instance); + CoreUserDelegate.registerHandler(AddonPrivateFilesUserHandler.instance); }, }, ], diff --git a/src/addons/privatefiles/services/handlers/mainmenu.ts b/src/addons/privatefiles/services/handlers/user.ts similarity index 50% rename from src/addons/privatefiles/services/handlers/mainmenu.ts rename to src/addons/privatefiles/services/handlers/user.ts index 0ec18da4c..4672385b3 100644 --- a/src/addons/privatefiles/services/handlers/mainmenu.ts +++ b/src/addons/privatefiles/services/handlers/user.ts @@ -14,44 +14,57 @@ import { Injectable } from '@angular/core'; -import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@features/mainmenu/services/mainmenu-delegate'; import { AddonPrivateFiles } from '@/addons/privatefiles/services/privatefiles'; import { makeSingleton } from '@singletons'; +import { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@features/user/services/user-delegate'; +import { CoreUserProfile } from '@features/user/services/user'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSites } from '@services/sites'; /** - * Handler to inject an option into main menu. + * Handler to inject an option into user menu. */ @Injectable({ providedIn: 'root' }) -export class AddonPrivateFilesMainMenuHandlerService implements CoreMainMenuHandler { +export class AddonPrivateFilesUserHandlerService implements CoreUserProfileHandler { static readonly PAGE_NAME = 'private'; name = 'AddonPrivateFiles'; - priority = 400; + priority = 300; + type = CoreUserDelegateService.TYPE_NEW_PAGE; + cacheEnabled = true; /** - * Check if the handler is enabled on a site level. - * - * @return Whether or not the handler is enabled on a site level. + * @inheritdoc */ async isEnabled(): Promise { return AddonPrivateFiles.isPluginEnabled(); } /** - * Returns the data needed to render the handler. - * - * @return Data needed to render the handler. + * @inheritdoc */ - getDisplayData(): CoreMainMenuHandlerData { + async isEnabledForUser(user: CoreUserProfile): Promise { + // Private files only available for the current user. + return user.id == CoreSites.getCurrentSiteUserId(); + } + + /** + * @inheritdoc + */ + getDisplayData(): CoreUserProfileHandlerData { return { icon: 'fas-folder', title: 'addon.privatefiles.files', - page: AddonPrivateFilesMainMenuHandlerService.PAGE_NAME, class: 'addon-privatefiles-handler', + action: (event): void => { + event.preventDefault(); + event.stopPropagation(); + CoreNavigator.navigateToSitePath(AddonPrivateFilesUserHandlerService.PAGE_NAME); + }, }; } } -export const AddonPrivateFilesMainMenuHandler = makeSingleton(AddonPrivateFilesMainMenuHandlerService); +export const AddonPrivateFilesUserHandler = makeSingleton(AddonPrivateFilesUserHandlerService); diff --git a/src/core/classes/modal-lateral-transition.ts b/src/core/classes/modal-lateral-transition.ts index 551d288d7..bc481f705 100644 --- a/src/core/classes/modal-lateral-transition.ts +++ b/src/core/classes/modal-lateral-transition.ts @@ -22,37 +22,61 @@ import { Platform } from '@singletons'; export function CoreModalLateralTransitionEnter(baseEl: HTMLElement): Animation { const OFF_RIGHT = Platform.isRTL ? '-100%' : '100%'; - const backdropAnimation = createAnimation() - .addElement(baseEl.querySelector('ion-backdrop')!) - .fromTo('opacity', 0.01, 0.4); + const otherAnimations: Animation[] = []; - const wrapperAnimation = createAnimation() - .addElement(baseEl.querySelector('.modal-wrapper')!) - .fromTo('transform', 'translateX(' + OFF_RIGHT + ')', 'translateX(0)') - .fromTo('opacity', 0.8, 1); + const backdrop = baseEl.querySelector('ion-backdrop'); + if (backdrop) { + const backdropAnimation = createAnimation() + .addElement(backdrop) + .fromTo('opacity', 0.01, 0.4); + + otherAnimations.push(backdropAnimation); + } + + const wrapper = baseEl.querySelector('.modal-wrapper'); + if (wrapper) { + const wrapperAnimation = createAnimation() + .addElement(wrapper) + .fromTo('transform', 'translateX(' + OFF_RIGHT + ')', 'translateX(0)') + .fromTo('opacity', 0.8, 1); + + otherAnimations.push(wrapperAnimation); + } return createAnimation() .addElement(baseEl) .easing('cubic-bezier(0.36,0.66,0.04,1)') .duration(300) - .addAnimation([backdropAnimation, wrapperAnimation]); + .addAnimation(otherAnimations); } export function CoreModalLateralTransitionLeave(baseEl: HTMLElement): Animation { const OFF_RIGHT = Platform.isRTL ? '-100%' : '100%'; - const backdropAnimation = createAnimation() - .addElement(baseEl.querySelector('ion-backdrop')!) - .fromTo('opacity', 0.4, 0.0); + const otherAnimations: Animation[] = []; - const wrapperAnimation = createAnimation() - .addElement(baseEl.querySelector('.modal-wrapper')!) - .beforeStyles({ opacity: 1 }) - .fromTo('transform', 'translateX(0)', 'translateX(' + OFF_RIGHT + ')'); + const backdrop = baseEl.querySelector('ion-backdrop'); + if (backdrop) { + const backdropAnimation = createAnimation() + .addElement(backdrop) + .fromTo('opacity', 0.4, 0.0); + + otherAnimations.push(backdropAnimation); + } + + const wrapper = baseEl.querySelector('.modal-wrapper'); + if (wrapper) { + const wrapperAnimation = createAnimation() + .addElement(wrapper) + .beforeStyles({ opacity: 1 }) + .fromTo('transform', 'translateX(0)', 'translateX(' + OFF_RIGHT + ')'); + + otherAnimations.push(wrapperAnimation); + } return createAnimation() .addElement(baseEl) .easing('cubic-bezier(0.36,0.66,0.04,1)') .duration(300) - .addAnimation([backdropAnimation, wrapperAnimation]); + .addAnimation(otherAnimations); } diff --git a/src/core/components/navbar-buttons/navbar-buttons.ts b/src/core/components/navbar-buttons/navbar-buttons.ts index 39b69167e..48439908e 100644 --- a/src/core/components/navbar-buttons/navbar-buttons.ts +++ b/src/core/components/navbar-buttons/navbar-buttons.ts @@ -56,7 +56,7 @@ const BUTTON_HIDDEN_CLASS = 'core-navbar-button-hidden'; }) export class CoreNavBarButtonsComponent implements OnInit, OnDestroy { - @ViewChild('contextMenuContainer', { read: ViewContainerRef }) container?: ViewContainerRef; + @ViewChild('contextMenuContainer', { read: ViewContainerRef }) container!: ViewContainerRef; // If the hidden input is true, hide all buttons. // eslint-disable-next-line @angular-eslint/no-input-rename @@ -113,7 +113,13 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy { // Make sure that context-menu is always at the end of buttons if any. const contextMenu = buttonsContainer.querySelector('core-context-menu'); - contextMenu?.parentElement?.appendChild(contextMenu); + const userMenu = buttonsContainer.querySelector('core-user-menu-button'); + + if (userMenu) { + contextMenu?.parentElement?.insertBefore(contextMenu, userMenu); + } else { + contextMenu?.parentElement?.appendChild(contextMenu); + } } else { this.logger.warn('The header was found, but it didn\'t have the right ion-buttons.', selector); } @@ -177,7 +183,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy { */ protected createMainContextMenu(): CoreContextMenuComponent { const factory = this.factoryResolver.resolveComponentFactory(CoreContextMenuComponent); - const componentRef = this.container!.createComponent(factory); + const componentRef = this.container.createComponent(factory); this.createdMainContextMenuElement = componentRef.location.nativeElement; diff --git a/src/core/components/user-avatar/user-avatar.scss b/src/core/components/user-avatar/user-avatar.scss index 5d99ba41d..f0e1ff20d 100644 --- a/src/core/components/user-avatar/user-avatar.scss +++ b/src/core/components/user-avatar/user-avatar.scss @@ -36,13 +36,19 @@ content: ""; } } - &.core-bar-button-image img { + &.core-bar-button-image { padding: 0; width: var(--core-header-toolbar-button-image-size); height: var(--core-header-toolbar-button-image-size); max-width: var(--core-header-toolbar-button-image-size); max-height: var(--core-header-toolbar-button-image-size); border-radius: 50%; + display: block; + + img { + padding: 4px; + border-radius: 50%; + } } .contact-status { diff --git a/src/core/features/courses/pages/dashboard/dashboard.html b/src/core/features/courses/pages/dashboard/dashboard.html index c4315e7d1..c79854f7a 100644 --- a/src/core/features/courses/pages/dashboard/dashboard.html +++ b/src/core/features/courses/pages/dashboard/dashboard.html @@ -1,4 +1,4 @@ - + diff --git a/src/core/features/courses/pages/list/list.html b/src/core/features/courses/pages/list/list.html index 114a5a78b..ffb6b4b22 100644 --- a/src/core/features/courses/pages/list/list.html +++ b/src/core/features/courses/pages/list/list.html @@ -5,20 +5,20 @@

{{ 'core.courses.availablecourses' | translate }}

{{ 'core.courses.mycourses' | translate }}

- + + + + + + + - - - - - - diff --git a/src/core/features/courses/pages/list/list.module.ts b/src/core/features/courses/pages/list/list.module.ts index c0d3a2c36..23dd52438 100644 --- a/src/core/features/courses/pages/list/list.module.ts +++ b/src/core/features/courses/pages/list/list.module.ts @@ -20,6 +20,7 @@ import { CoreCoursesComponentsModule } from '../../components/components.module' import { CoreSearchComponentsModule } from '@features/search/components/components.module'; import { CoreCoursesListPage } from './list'; +import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module'; const routes: Routes = [ { @@ -34,6 +35,7 @@ const routes: Routes = [ CoreSharedModule, CoreCoursesComponentsModule, CoreSearchComponentsModule, + CoreMainMenuComponentsModule, ], declarations: [ CoreCoursesListPage, diff --git a/src/core/features/grades/grades-lazy.module.ts b/src/core/features/grades/grades-lazy.module.ts index e60242302..d825a2621 100644 --- a/src/core/features/grades/grades-lazy.module.ts +++ b/src/core/features/grades/grades-lazy.module.ts @@ -23,13 +23,13 @@ import { CoreGradesCoursePage } from './pages/course/course.page'; import { CoreGradesCoursePageModule } from './pages/course/course.module'; import { CoreGradesCoursesPage } from './pages/courses/courses.page'; import { CoreGradesGradePage } from './pages/grade/grade.page'; -import { CoreGradesMainMenuHandlerService } from './services/handlers/mainmenu'; +import { CoreGradesUserHandlerService } from './services/handlers/user'; const mobileRoutes: Routes = [ { path: '', data: { - mainMenuTabRoot: CoreGradesMainMenuHandlerService.PAGE_NAME, + mainMenuTabRoot: CoreGradesUserHandlerService.PAGE_NAME, }, component: CoreGradesCoursesPage, }, @@ -47,7 +47,7 @@ const tabletRoutes: Routes = [ { path: '', data: { - mainMenuTabRoot: CoreGradesMainMenuHandlerService.PAGE_NAME, + mainMenuTabRoot: CoreGradesUserHandlerService.PAGE_NAME, }, component: CoreGradesCoursesPage, children: [ diff --git a/src/core/features/grades/grades.module.ts b/src/core/features/grades/grades.module.ts index b359d575c..d7ca58cf3 100644 --- a/src/core/features/grades/grades.module.ts +++ b/src/core/features/grades/grades.module.ts @@ -19,14 +19,12 @@ import { CoreCourseIndexRoutingModule } from '@features/course/pages/index/index import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; -import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate'; import { CoreUserDelegate } from '@features/user/services/user-delegate'; import { CoreGradesProvider } from './services/grades'; import { CoreGradesHelperProvider } from './services/grades-helper'; import { CoreGradesCourseOptionHandler } from './services/handlers/course-option'; -import { CoreGradesMainMenuHandler, CoreGradesMainMenuHandlerService } from './services/handlers/mainmenu'; import { CoreGradesOverviewLinkHandler } from './services/handlers/overview-link'; -import { CoreGradesUserHandler } from './services/handlers/user'; +import { CoreGradesUserHandler, CoreGradesUserHandlerService } from './services/handlers/user'; import { CoreGradesUserLinkHandler } from './services/handlers/user-link'; export const CORE_GRADES_SERVICES: Type[] = [ @@ -36,7 +34,7 @@ export const CORE_GRADES_SERVICES: Type[] = [ const routes: Routes = [ { - path: CoreGradesMainMenuHandlerService.PAGE_NAME, + path: CoreGradesUserHandlerService.PAGE_NAME, loadChildren: () => import('@features/grades/grades-lazy.module').then(m => m.CoreGradesLazyModule), }, { @@ -63,7 +61,6 @@ const courseIndexRoutes: Routes = [ provide: APP_INITIALIZER, multi: true, useValue: () => { - CoreMainMenuDelegate.registerHandler(CoreGradesMainMenuHandler.instance); CoreUserDelegate.registerHandler(CoreGradesUserHandler.instance); CoreContentLinksDelegate.registerHandler(CoreGradesUserLinkHandler.instance); CoreContentLinksDelegate.registerHandler(CoreGradesOverviewLinkHandler.instance); diff --git a/src/core/features/grades/services/handlers/mainmenu.ts b/src/core/features/grades/services/handlers/mainmenu.ts deleted file mode 100644 index 6e7a7b915..000000000 --- a/src/core/features/grades/services/handlers/mainmenu.ts +++ /dev/null @@ -1,56 +0,0 @@ -// (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 { Injectable } from '@angular/core'; -import { CoreGrades } from '@features/grades/services/grades'; -import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@features/mainmenu/services/mainmenu-delegate'; -import { makeSingleton } from '@singletons'; - -/** - * Handler to inject an option into main menu. - */ -@Injectable({ providedIn: 'root' }) -export class CoreGradesMainMenuHandlerService implements CoreMainMenuHandler { - - static readonly PAGE_NAME = 'grades'; - - name = 'CoreGrades'; - priority = 600; - - /** - * Check if the handler is enabled on a site level. - * - * @return Whether or not the handler is enabled on a site level. - */ - isEnabled(): Promise { - return CoreGrades.isCourseGradesEnabled(); - } - - /** - * Returns the data needed to render the handler. - * - * @return Data needed to render the handler. - */ - getDisplayData(): CoreMainMenuHandlerData { - return { - icon: 'fas-chart-bar', - title: 'core.grades.grades', - page: CoreGradesMainMenuHandlerService.PAGE_NAME, - class: 'core-grades-coursesgrades-handler', - }; - } - -} - -export const CoreGradesMainMenuHandler = makeSingleton(CoreGradesMainMenuHandlerService); diff --git a/src/core/features/grades/services/handlers/user.ts b/src/core/features/grades/services/handlers/user.ts index 544044018..fab0752f6 100644 --- a/src/core/features/grades/services/handlers/user.ts +++ b/src/core/features/grades/services/handlers/user.ts @@ -22,6 +22,7 @@ import { CoreUserProfileHandlerData, } from '@features/user/services/user-delegate'; import { CoreNavigator } from '@services/navigator'; +import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; @@ -31,6 +32,8 @@ import { makeSingleton } from '@singletons'; @Injectable({ providedIn: 'root' }) export class CoreGradesUserHandlerService implements CoreUserProfileHandler { + static readonly PAGE_NAME = 'grades'; + name = 'CoreGrades:viewGrades'; priority = 400; type = CoreUserDelegateService.TYPE_NEW_PAGE; @@ -47,32 +50,54 @@ export class CoreGradesUserHandlerService implements CoreUserProfileHandler { * @inheritdoc */ async isEnabledForCourse(courseId?: number): Promise { - return CoreUtils.ignoreErrors(CoreGrades.isPluginEnabledForCourse(courseId), false); + if (courseId) { + return CoreUtils.ignoreErrors(CoreGrades.isPluginEnabledForCourse(courseId), false); + } else { + return CoreGrades.isCourseGradesEnabled(); + } } /** * @inheritdoc */ async isEnabledForUser(user: CoreUserProfile, courseId?: number): Promise { - return CoreUtils.promiseWorks(CoreGrades.getCourseGradesTable(courseId!, user.id)); + if (courseId) { + return CoreUtils.promiseWorks(CoreGrades.getCourseGradesTable(courseId, user.id)); + } + + // All course grades only available for the current user. + return user.id == CoreSites.getCurrentSiteUserId(); } /** * @inheritdoc */ - getDisplayData(): CoreUserProfileHandlerData { - return { - icon: 'fas-chart-bar', - title: 'core.grades.grades', - class: 'core-grades-user-handler', - action: (event, user, courseId): void => { - event.preventDefault(); - event.stopPropagation(); - CoreNavigator.navigateToSitePath(`/user-grades/${courseId}`, { - params: { userId: user.id }, - }); - }, - }; + getDisplayData(user: CoreUserProfile, courseId?: number): CoreUserProfileHandlerData { + if (courseId) { + return { + icon: 'fas-chart-bar', + title: 'core.grades.grades', + class: 'core-grades-user-handler', + action: (event, user, courseId): void => { + event.preventDefault(); + event.stopPropagation(); + CoreNavigator.navigateToSitePath(`/user-grades/${courseId}`, { + params: { userId: user.id }, + }); + }, + }; + } else { + return { + icon: 'fas-chart-bar', + title: 'core.grades.grades', + class: 'core-grades-coursesgrades-handler', + action: (event): void => { + event.preventDefault(); + event.stopPropagation(); + CoreNavigator.navigateToSitePath(CoreGradesUserHandlerService.PAGE_NAME); + }, + }; + } } } diff --git a/src/core/features/login/components/components.module.ts b/src/core/features/login/components/components.module.ts new file mode 100644 index 000000000..37d326e79 --- /dev/null +++ b/src/core/features/login/components/components.module.ts @@ -0,0 +1,36 @@ +// (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 { CoreSharedModule } from '@/core/shared.module'; +import { CoreLoginSiteOnboardingComponent } from './site-onboarding/site-onboarding'; +import { CoreLoginSiteHelpComponent } from './site-help/site-help'; +import { CoreLoginSitesComponent } from './sites/sites'; + +@NgModule({ + declarations: [ + CoreLoginSiteOnboardingComponent, + CoreLoginSiteHelpComponent, + CoreLoginSitesComponent, + ], + imports: [ + CoreSharedModule, + ], + exports: [ + CoreLoginSiteOnboardingComponent, + CoreLoginSiteHelpComponent, + CoreLoginSitesComponent, + ], +}) +export class CoreLoginComponentsModule {} diff --git a/src/core/features/login/components/sites/sites.html b/src/core/features/login/components/sites/sites.html new file mode 100644 index 000000000..5cf1c7fae --- /dev/null +++ b/src/core/features/login/components/sites/sites.html @@ -0,0 +1,93 @@ + + + + + + + + +

{{ 'core.mainmenu.switchaccount' | translate }}

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

+ +

+

{{ + accountsList.currentSite.siteUrlWithoutProtocol }} +

+
+
+ + + + {{ 'core.pictureof' | translate:{$a: accountsList.currentSite.fullName} }} + + +

{{accountsList.currentSite.fullName}}

+
+ +
+ + +
+ + + + +

+ +

+

{{ sites[0].siteUrlWithoutProtocol }}

+
+
+ + +
+ +
+
+ + + + {{ 'core.login.add' | translate }} + + +
+ + + + + + {{ 'core.pictureof' | translate:{$a: site.fullName} }} + + +

{{site.fullName}}

+
+ + + {{ 'core.login.sitebadgedescription' | translate:{ count: site.badge } + }} + + + + +
+
diff --git a/src/core/features/login/components/sites/sites.ts b/src/core/features/login/components/sites/sites.ts new file mode 100644 index 000000000..1f14b51a7 --- /dev/null +++ b/src/core/features/login/components/sites/sites.ts @@ -0,0 +1,131 @@ +// (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 { CoreDomUtils } from '@services/utils/dom'; +import { Component, OnInit } from '@angular/core'; + +import { CoreSiteBasicInfo, CoreSites } from '@services/sites'; +import { CoreAccountsList, CoreLoginHelper } from '@features/login/services/login-helper'; +import { CoreNavigator } from '@services/navigator'; +import { CoreFilter } from '@features/filter/services/filter'; +import { CoreAnimations } from '@components/animations'; +import { ModalController } from '@singletons'; + +/** + * Component that displays a "splash screen" while the app is being initialized. + */ +@Component({ + selector: 'core-login-sites', + templateUrl: 'sites.html', + animations: [CoreAnimations.SLIDE_IN_OUT, CoreAnimations.SHOW_HIDE], +}) +export class CoreLoginSitesComponent implements OnInit { + + accountsList: CoreAccountsList = { + sameSite: [], + otherSites: [], + count: 0, + }; + + showDelete = false; + currentSiteId: string; + loaded = false; + + constructor() { + this.currentSiteId = CoreSites.getRequiredCurrentSite().getId(); + } + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + this.accountsList = await CoreLoginHelper.getAccountsList(this.currentSiteId); + this.loaded = true; + } + + /** + * Go to the page to add a site. + * + * @param event Click event. + */ + async add(event: Event): Promise { + await this.close(event, true); + + await CoreLoginHelper.goToAddSite(true, true); + } + + /** + * Delete a site. + * + * @param event Click event. + * @param site Site to delete. + * @return Promise resolved when done. + */ + async deleteSite(event: Event, site: CoreSiteBasicInfo): Promise { + event.stopPropagation(); + + let siteName = site.siteName || ''; + + siteName = await CoreFilter.formatText(siteName, { clean: true, singleLine: true, filter: false }, [], site.id); + + try { + await CoreDomUtils.showDeleteConfirm('core.login.confirmdeletesite', { sitename: siteName }); + } catch { + // User cancelled, stop. + return; + } + + try { + await CoreLoginHelper.deleteAccountFromList(this.accountsList, site); + + this.showDelete = false; + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'core.login.errordeletesite', true); + } + } + + /** + * Login in a site. + * + * @param event Click event. + * @param siteId The site ID. + * @return Promise resolved when done. + */ + async login(event: Event, siteId: string): Promise { + await this.close(event, true); + + // This navigation will logout and navigate to the site home. + await CoreNavigator.navigateToSiteHome({ preferCurrentTab: false , siteId }); + } + + /** + * Toggle delete. + */ + toggleDelete(): void { + this.showDelete = !this.showDelete; + } + + /** + * Close modal. + * + * @param event Click event. + */ + async close(event: Event, closeAll = false): Promise { + event.preventDefault(); + event.stopPropagation(); + + await ModalController.dismiss(closeAll); + } + +} diff --git a/src/core/features/login/lang.json b/src/core/features/login/lang.json index 8ebeb7fa5..53a7bf0ed 100644 --- a/src/core/features/login/lang.json +++ b/src/core/features/login/lang.json @@ -1,4 +1,6 @@ { + "accounts": "Accounts", + "add": "Add a new account", "auth_email": "Email-based self-registration", "authenticating": "Authenticating", "cancel": "Cancel", @@ -8,7 +10,7 @@ "changepasswordinstructions": "You cannot change your password in the app. Please click the following button to open the site in a web browser to change your password. Take into account you need to close the browser after changing the password as you will not be redirected to the app.", "changepasswordlogoutinstructions": "If you prefer to change site or log out, please click the following button:", "changepasswordreconnectinstructions": "Click the following button to reconnect to the site. (Take into account that if you didn't change your password successfully, you would return to the previous screen).", - "confirmdeletesite": "Are you sure you want to delete the site {{sitename}}?", + "confirmdeletesite": "Are you sure you want to remove the account on {{sitename}}?", "connect": "Connect!", "connecttomoodle": "Connect to Moodle", "contactyouradministrator": "Contact your site administrator for further help.", @@ -23,7 +25,7 @@ "emailconfirmsentsuccess": "Confirmation email sent successfully", "emailnotmatch": "Emails do not match", "erroraccesscontrolalloworigin": "The cross-origin call you're trying to perform has been rejected. Please check https://docs.moodle.org/dev/Moodle_Mobile_development_using_Chrome_or_Chromium", - "errordeletesite": "An error occurred while deleting this site. Please try again.", + "errordeletesite": "An error occurred while deleting this account. Please try again.", "errorexampleurl": "The URL https://campus.example.edu is only an example URL, it's not a real site. Please use the URL of your school or organization's site.", "errorqrnoscheme": "This URL isn't a valid login URL.", "errorupdatesite": "An error occurred while updating the site's token.", @@ -95,11 +97,12 @@ "reconnect": "Reconnect", "reconnectdescription": "Your authentication token is invalid or has expired. You have to reconnect to the site.", "reconnectssodescription": "Your authentication token is invalid or has expired. You have to reconnect to the site. You need to log in to the site in a browser window.", + "removeaccount": "Remove account", "resendemail": "Resend email", "searchby": "Search by:", "security_question": "Security question", "selectacountry": "Select a country", - "selectsite": "Please select your site:", + "selectsite": "Please select your account:", "signupplugindisabled": "{{$a}} is not enabled.", "signuprequiredfieldnotsupported": "The signup form contains a required custom field that isn't supported in the app. Please create your account using a web browser.", "siteaddress": "Your site", @@ -114,6 +117,7 @@ "startsignup": "Create new account", "stillcantconnect": "Still can't connect?", "supplyinfo": "More details", + "toggleremove": "Edit accounts list", "username": "Username", "usernameoremail": "Enter either username or email address", "usernamerequired": "Username required", diff --git a/src/core/features/login/login-lazy.module.ts b/src/core/features/login/login-lazy.module.ts index bab14255b..bad7099b5 100644 --- a/src/core/features/login/login-lazy.module.ts +++ b/src/core/features/login/login-lazy.module.ts @@ -16,9 +16,8 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CoreSharedModule } from '@/core/shared.module'; -import { CoreLoginSiteHelpComponent } from './components/site-help/site-help'; -import { CoreLoginSiteOnboardingComponent } from './components/site-onboarding/site-onboarding'; import { CoreLoginHasSitesGuard } from './guards/has-sites'; +import { CoreLoginComponentsModule } from './components/components.module'; const routes: Routes = [ { @@ -67,11 +66,8 @@ const routes: Routes = [ @NgModule({ imports: [ CoreSharedModule, + CoreLoginComponentsModule, RouterModule.forChild(routes), ], - declarations: [ - CoreLoginSiteHelpComponent, - CoreLoginSiteOnboardingComponent, - ], }) export class CoreLoginLazyModule {} diff --git a/src/core/features/login/login.module.ts b/src/core/features/login/login.module.ts index caa91ea91..fdbb39639 100644 --- a/src/core/features/login/login.module.ts +++ b/src/core/features/login/login.module.ts @@ -35,7 +35,9 @@ const appRoutes: Routes = [ ]; @NgModule({ - imports: [AppRoutingModule.forChild(appRoutes)], + imports: [ + AppRoutingModule.forChild(appRoutes), + ], providers: [ { provide: APP_INITIALIZER, diff --git a/src/core/features/login/pages/site-policy/site-policy.ts b/src/core/features/login/pages/site-policy/site-policy.ts index d58ee4c4e..8c4097f9e 100644 --- a/src/core/features/login/pages/site-policy/site-policy.ts +++ b/src/core/features/login/pages/site-policy/site-policy.ts @@ -35,17 +35,17 @@ export class CoreLoginSitePolicyPage implements OnInit { showInline?: boolean; policyLoaded?: boolean; protected siteId?: string; - protected currentSite?: CoreSite; + protected currentSite!: CoreSite; /** - * Component initialized. + * @inheritdoc */ ngOnInit(): void { - this.siteId = CoreNavigator.getRouteParam('siteId'); - this.currentSite = CoreSites.getCurrentSite(); - if (!this.currentSite) { + try { + this.currentSite = CoreSites.getRequiredCurrentSite(); + } catch { // Not logged in, stop. this.cancel(); @@ -86,7 +86,7 @@ export class CoreLoginSitePolicyPage implements OnInit { const extension = CoreMimetypeUtils.getExtension(mimeType, this.sitePolicy); this.showInline = extension == 'html' || extension == 'htm'; - } catch (error) { + } catch { // Unable to get mime type, assume it's not supported. this.showInline = false; } finally { @@ -118,7 +118,7 @@ export class CoreLoginSitePolicyPage implements OnInit { // Success accepting, go to site initial page. // Invalidate cache since some WS don't return error if site policy is not accepted. - await CoreUtils.ignoreErrors(this.currentSite!.invalidateWsCache()); + await CoreUtils.ignoreErrors(this.currentSite.invalidateWsCache()); await CoreNavigator.navigateToSiteHome(); } catch (error) { diff --git a/src/core/features/login/pages/sites/sites.html b/src/core/features/login/pages/sites/sites.html index 704daedff..a47a65f2f 100644 --- a/src/core/features/login/pages/sites/sites.html +++ b/src/core/features/login/pages/sites/sites.html @@ -4,11 +4,11 @@ -

{{ 'core.settings.sites' | translate }}

+

{{ 'core.login.accounts' | translate }}

- + @@ -18,31 +18,43 @@ - - - - {{ 'core.pictureof' | translate:{$a: site.fullName} }} - - -

{{site.fullName}}

-

-

{{site.siteUrl}}

-
- - - {{ 'core.login.sitebadgedescription' | translate:{ count: site.badge } }} - - - - -
-
+ + + + + +

+ +

+

{{ sites[0].siteUrlWithoutProtocol }}

+
+
+ + + + {{ 'core.pictureof' | translate:{$a: site.fullName} }} + + +

{{site.fullName}}

+
+ + + {{ 'core.login.sitebadgedescription' | translate:{ count: site.badge } + }} + + + + +
+
+
+
- + - {{ 'core.add' | translate }} + {{ 'core.login.add' | translate }}
diff --git a/src/core/features/login/pages/sites/sites.ts b/src/core/features/login/pages/sites/sites.ts index 447d79f7d..a6d3e1eeb 100644 --- a/src/core/features/login/pages/sites/sites.ts +++ b/src/core/features/login/pages/sites/sites.ts @@ -13,14 +13,11 @@ // limitations under the License. import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUtils } from '@services/utils/utils'; import { Component, OnInit } from '@angular/core'; import { CoreSiteBasicInfo, CoreSites } from '@services/sites'; -import { CoreLogger } from '@singletons/logger'; -import { CoreLoginHelper } from '@features/login/services/login-helper'; +import { CoreAccountsList, CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreNavigator } from '@services/navigator'; -import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CoreFilter } from '@features/filter/services/filter'; import { CoreAnimations } from '@components/animations'; @@ -30,40 +27,33 @@ import { CoreAnimations } from '@components/animations'; @Component({ selector: 'page-core-login-sites', templateUrl: 'sites.html', - animations: [CoreAnimations.SLIDE_IN_OUT], + animations: [CoreAnimations.SLIDE_IN_OUT, CoreAnimations.SHOW_HIDE], }) export class CoreLoginSitesPage implements OnInit { - sites: CoreSiteBasicInfo[] = []; + accountsList: CoreAccountsList = { + sameSite: [], + otherSites: [], + count: 0, + }; + showDelete = false; - - protected logger: CoreLogger; - - constructor() { - this.logger = CoreLogger.getInstance('CoreLoginSitesPage'); - } + loaded = false; /** - * Component being initialized. - * - * @return Promise resolved when done. + * @inheritdoc */ async ngOnInit(): Promise { if (CoreNavigator.getRouteBooleanParam('openAddSite')) { this.add(); } - const sites = await CoreUtils.ignoreErrors(CoreSites.getSortedSites(), [] as CoreSiteBasicInfo[]); + this.accountsList = await CoreLoginHelper.getAccountsList(); + this.loaded = true; - // Remove protocol from the url to show more url text. - this.sites = await Promise.all(sites.map(async (site) => { - site.siteUrl = site.siteUrl.replace(/^https?:\/\//, ''); - site.badge = await CoreUtils.ignoreErrors(CorePushNotifications.getSiteCounter(site.id)) || 0; - - return site; - })); - - this.showDelete = false; + if (this.accountsList.count == 0) { + this.add(); + } } /** @@ -76,12 +66,12 @@ export class CoreLoginSitesPage implements OnInit { /** * Delete a site. * - * @param e Click event. + * @param event Click event. * @param site Site to delete. * @return Promise resolved when done. */ - async deleteSite(e: Event, site: CoreSiteBasicInfo): Promise { - e.stopPropagation(); + async deleteSite(event: Event, site: CoreSiteBasicInfo): Promise { + event.stopPropagation(); let siteName = site.siteName || ''; @@ -95,20 +85,15 @@ export class CoreLoginSitesPage implements OnInit { } try { - await CoreSites.deleteSite(site.id); + await CoreLoginHelper.deleteAccountFromList(this.accountsList, site); - const index = this.sites.findIndex((listedSite) => listedSite.id == site.id); - index >= 0 && this.sites.splice(index, 1); this.showDelete = false; // If there are no sites left, go to add site. - const hasSites = await CoreSites.hasSites(); - - if (!hasSites) { + if (this.accountsList.count == 0) { CoreLoginHelper.goToAddSite(true, true); } } catch (error) { - this.logger.error('Error deleting site ' + site.id, error); CoreDomUtils.showErrorModalDefault(error, 'core.login.errordeletesite', true); } } @@ -116,10 +101,14 @@ export class CoreLoginSitesPage implements OnInit { /** * Login in a site. * + * @param event Click event. * @param siteId The site ID. * @return Promise resolved when done. */ - async login(siteId: string): Promise { + async login(event: Event, siteId: string): Promise { + event.preventDefault(); + event.stopPropagation(); + const modal = await CoreDomUtils.showModalLoading(); try { @@ -131,7 +120,6 @@ export class CoreLoginSitesPage implements OnInit { return; } } catch (error) { - this.logger.error('Error loading site ' + siteId, error); CoreDomUtils.showErrorModalDefault(error, 'Error loading site.'); } finally { modal.dismiss(); diff --git a/src/core/features/login/services/login-helper.ts b/src/core/features/login/services/login-helper.ts index d41c0b742..627547671 100644 --- a/src/core/features/login/services/login-helper.ts +++ b/src/core/features/login/services/login-helper.ts @@ -19,7 +19,7 @@ import { Md5 } from 'ts-md5/dist/md5'; import { CoreApp, CoreStoreConfig } from '@services/app'; import { CoreConfig } from '@services/config'; import { CoreEvents, CoreEventSessionExpiredData, CoreEventSiteData } from '@singletons/events'; -import { CoreSites, CoreLoginSiteInfo } from '@services/sites'; +import { CoreSites, CoreLoginSiteInfo, CoreSiteBasicInfo } from '@services/sites'; import { CoreWS, CoreWSExternalWarning } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; @@ -35,6 +35,8 @@ import { CoreUrl } from '@singletons/url'; import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreCustomURLSchemes } from '@services/urlschemes'; +import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; +import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; /** * Helper provider that provides some common features regarding authentication. @@ -311,7 +313,7 @@ export class CoreLoginHelperProvider { site = site || CoreSites.getCurrentSite(); const config = site?.getStoredConfig(); - return 'core.mainmenu.' + (config && config.tool_mobile_forcelogout == '1' ? 'logout' : 'changesite'); + return 'core.mainmenu.' + (config && config.tool_mobile_forcelogout == '1' ? 'logout' : 'switchaccount'); } /** @@ -407,8 +409,25 @@ export class CoreLoginHelperProvider { * @param showKeyboard Whether to show keyboard in the new page. Only if no fixed URL set. * @return Promise resolved when done. */ - async goToAddSite(setRoot?: boolean, showKeyboard?: boolean): Promise { - const [path, params] = this.getAddSiteRouteInfo(showKeyboard); + async goToAddSite(setRoot = false, showKeyboard = false): Promise { + let path = '/login/sites'; + let params: Params = { openAddSite: true , showKeyboard }; + + if (CoreSites.isLoggedIn()) { + + if (CoreSitePlugins.hasSitePluginsLoaded) { + // The site has site plugins so the app will be restarted. Store the data and logout. + CoreApp.storeRedirect(CoreConstants.NO_SITE_ID, path, { params }); + + await CoreSites.logout(); + + return; + } + + await CoreSites.logout(); + } else { + [path, params] = this.getAddSiteRouteInfo(showKeyboard); + } await CoreNavigator.navigate(path, { params, reset: setRoot }); } @@ -1317,43 +1336,120 @@ export class CoreLoginHelperProvider { } } + /** + * Get the accounts list classified per site. + * + * @param currentSiteId If loggedin, current Site Id. + * @return Promise resolved with account list. + */ + async getAccountsList(currentSiteId?: string): Promise { + const sites = await CoreUtils.ignoreErrors(CoreSites.getSortedSites(), [] as CoreSiteBasicInfo[]); + + const accountsList: CoreAccountsList = { + sameSite: [], + otherSites: [], + count: sites.length, + }; + + let siteUrl = ''; + + if (currentSiteId) { + const index = sites.findIndex((site) => site.id == currentSiteId); + + accountsList.currentSite = sites.splice(index, 1)[0]; + siteUrl = accountsList.currentSite.siteUrlWithoutProtocol; + } + + const otherSites: Record = {}; + + // Add site counter and classify sites. + await Promise.all(sites.map(async (site) => { + site.badge = await CoreUtils.ignoreErrors(CorePushNotifications.getSiteCounter(site.id)) || 0; + + if (site.siteUrlWithoutProtocol == siteUrl) { + accountsList.sameSite.push(site); + } else { + if (!otherSites[site.siteUrlWithoutProtocol]) { + otherSites[site.siteUrlWithoutProtocol] = []; + } + + otherSites[site.siteUrlWithoutProtocol].push(site); + } + + return; + })); + + accountsList.otherSites = CoreUtils.objectToArray(otherSites); + + return accountsList; + } + + /** + * Find and delete a site from the list of sites. + * + * @param accountsList Account list. + * @param site Site to be deleted. + * @return Resolved when done. + */ + async deleteAccountFromList(accountsList: CoreAccountsList, site: CoreSiteBasicInfo): Promise { + await CoreSites.deleteSite(site.id); + + const siteUrl = site.siteUrlWithoutProtocol; + let index = 0; + + // Found on same site. + if (accountsList.sameSite.length > 0 && accountsList.sameSite[0].siteUrlWithoutProtocol == siteUrl) { + index = accountsList.sameSite.findIndex((listedSite) => listedSite.id == site.id); + if (index >= 0) { + accountsList.sameSite.splice(index, 1); + accountsList.count--; + } + + return; + } + + const otherSiteIndex = accountsList.otherSites.findIndex((sites) => + sites.length > 0 && sites[0].siteUrlWithoutProtocol == siteUrl); + if (otherSiteIndex < 0) { + // Site Url not found. + return; + } + + index = accountsList.otherSites[otherSiteIndex].findIndex((listedSite) => listedSite.id == site.id); + if (index >= 0) { + accountsList.otherSites[otherSiteIndex].splice(index, 1); + accountsList.count--; + } + + if (accountsList.otherSites[otherSiteIndex].length == 0) { + accountsList.otherSites.splice(otherSiteIndex, 1); + } + } + } export const CoreLoginHelper = makeSingleton(CoreLoginHelperProvider); +/** + * Accounts list for selecting sites interfaces. + */ +export type CoreAccountsList = { + currentSite?: CoreSiteBasicInfo; // If logged in, current site info. + sameSite: CoreSiteBasicInfo[]; // If logged in, accounts info on the same site. + otherSites: CoreSiteBasicInfo[][]; // Other accounts in other sites. + count: number; // Number of sites. +}; + /** * Data related to a SSO authentication. */ export interface CoreLoginSSOData { - /** - * The site's URL. - */ - siteUrl: string; - - /** - * User's token. - */ - token?: string; - - /** - * User's private token. - */ - privateToken?: string; - - /** - * Name of the page to go after authenticated. - */ - pageName?: string; - - /** - * Options of the navigation to the page. - */ - pageOptions?: CoreNavigationOptions; - - /** - * Other params added to the login url. - */ - ssoUrlParams?: CoreUrlParams; + siteUrl: string; // The site's URL. + token?: string; // User's token. + privateToken?: string; // User's private token. + pageName?: string; // Name of the page to go after authenticated. + pageOptions?: CoreNavigationOptions; // Options of the navigation to the page. + ssoUrlParams?: CoreUrlParams; // Other params added to the login url. }; /** diff --git a/src/core/features/mainmenu/components/components.module.ts b/src/core/features/mainmenu/components/components.module.ts new file mode 100644 index 000000000..613c1c790 --- /dev/null +++ b/src/core/features/mainmenu/components/components.module.ts @@ -0,0 +1,35 @@ +// (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 { CoreSharedModule } from '@/core/shared.module'; +import { CoreMainMenuUserButtonComponent } from './user-menu-button/user-menu-button'; +import { CoreMainMenuUserMenuComponent } from './user-menu/user-menu'; +import { CoreLoginComponentsModule } from '@features/login/components/components.module'; + +@NgModule({ + declarations: [ + CoreMainMenuUserButtonComponent, + CoreMainMenuUserMenuComponent, + ], + imports: [ + CoreSharedModule, + CoreLoginComponentsModule, + ], + exports: [ + CoreMainMenuUserButtonComponent, + CoreMainMenuUserMenuComponent, + ], +}) +export class CoreMainMenuComponentsModule {} diff --git a/src/core/features/mainmenu/components/user-menu-button/user-menu-button.html b/src/core/features/mainmenu/components/user-menu-button/user-menu-button.html new file mode 100644 index 000000000..998f7e7c0 --- /dev/null +++ b/src/core/features/mainmenu/components/user-menu-button/user-menu-button.html @@ -0,0 +1,3 @@ + + diff --git a/src/core/features/mainmenu/components/user-menu-button/user-menu-button.scss b/src/core/features/mainmenu/components/user-menu-button/user-menu-button.scss new file mode 100644 index 000000000..15aa50df5 --- /dev/null +++ b/src/core/features/mainmenu/components/user-menu-button/user-menu-button.scss @@ -0,0 +1,3 @@ +:host-context(ion-tabs.placement-side div.tabs-inner) { + display: none; +} \ No newline at end of file diff --git a/src/core/features/mainmenu/components/user-menu-button/user-menu-button.ts b/src/core/features/mainmenu/components/user-menu-button/user-menu-button.ts new file mode 100644 index 000000000..a27782b73 --- /dev/null +++ b/src/core/features/mainmenu/components/user-menu-button/user-menu-button.ts @@ -0,0 +1,65 @@ +// (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, OnInit } from '@angular/core'; +import { CoreSiteInfo } from '@classes/site'; +import { IonRouterOutlet } from '@ionic/angular'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreMainMenuUserMenuComponent } from '../user-menu/user-menu'; + +/** + * Component to display an avatar on the header to open user menu. + * + * Example: + */ +@Component({ + selector: 'core-user-menu-button', + templateUrl: 'user-menu-button.html', + styleUrls: ['user-menu-button.scss'], +}) +export class CoreMainMenuUserButtonComponent implements OnInit { + + siteInfo?: CoreSiteInfo; + isMainScreen = false; + + constructor(protected routerOutlet: IonRouterOutlet) { + const currentSite = CoreSites.getRequiredCurrentSite(); + + this.siteInfo = currentSite.getInfo(); + } + + /** + * @inheritdoc + */ + ngOnInit(): void { + this.isMainScreen = !this.routerOutlet.canGoBack(); + } + + /** + * Open User menu + * + * @param event Click event. + */ + openUserMenu(event: Event): void { + event.preventDefault(); + event.stopPropagation(); + + CoreDomUtils.openSideModal({ + component: CoreMainMenuUserMenuComponent, + cssClass: 'core-modal-lateral-sm', + }); + } + +} diff --git a/src/core/features/mainmenu/components/user-menu/user-menu.html b/src/core/features/mainmenu/components/user-menu/user-menu.html new file mode 100644 index 000000000..34a9184f7 --- /dev/null +++ b/src/core/features/mainmenu/components/user-menu/user-menu.html @@ -0,0 +1,73 @@ + + + + + + + +

+ {{'core.user.account' | translate}} +

+
+
+ + + + + + + +

{{ 'core.user.profile' | translate }}

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

{{ handler.title | translate }}

+
+ + + {{ handler.badgeA11yText | translate: {$a : handler.badge } }} + + + +
+ + + + +

{{ 'core.settings.preferences' | translate }}

+
+
+
+
+ + + + {{ 'core.mainmenu.logout' | translate }} + + diff --git a/src/core/features/mainmenu/components/user-menu/user-menu.scss b/src/core/features/mainmenu/components/user-menu/user-menu.scss new file mode 100644 index 000000000..74e3b93a4 --- /dev/null +++ b/src/core/features/mainmenu/components/user-menu/user-menu.scss @@ -0,0 +1,26 @@ +@import "~theme/globals"; + +:host { + .core-user-menu-preferences { + --inner-border-width: 0; + --border-width: 1px 0 0 0; + } +} + +@if ($core-user-hide-siteinfo) { + .core-usermenu-siteinfo { + display: none; + } +} + +@if ($core-user-hide-sitename) { + .core-usermenu-sitename { + display: none; + } +} + +@if ($core-user-hide-siteurl) { + .core-usermenu-siteurl { + display: none; + } +} diff --git a/src/core/features/mainmenu/components/user-menu/user-menu.ts b/src/core/features/mainmenu/components/user-menu/user-menu.ts new file mode 100644 index 000000000..1ec0e39ad --- /dev/null +++ b/src/core/features/mainmenu/components/user-menu/user-menu.ts @@ -0,0 +1,191 @@ +// (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 { CoreSiteInfo } from '@classes/site'; +import { CoreLoginSitesComponent } from '@features/login/components/sites/sites'; +import { CoreLoginHelper } from '@features/login/services/login-helper'; +import { CoreUser, CoreUserProfile } from '@features/user/services/user'; +import { CoreUserProfileHandlerData, CoreUserDelegate, CoreUserDelegateService } from '@features/user/services/user-delegate'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { ModalController } from '@singletons'; +import { Subscription } from 'rxjs'; + +/** + * Component to display a user menu. + */ +@Component({ + selector: 'core-main-menu-user-menu', + templateUrl: 'user-menu.html', + styleUrls: ['user-menu.scss'], +}) +export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy { + + siteInfo?: CoreSiteInfo; + siteName?: string; + siteUrl?: string; + handlers: CoreUserProfileHandlerData[] = []; + handlersLoaded = false; + loaded = false; + user?: CoreUserProfile; + moreSites = false; + + protected subscription!: Subscription; + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + // Check if there are more sites to switch. + const sites = await CoreSites.getSites(); + this.moreSites = sites.length > 1; + + const currentSite = CoreSites.getRequiredCurrentSite(); + this.siteInfo = currentSite.getInfo(); + this.siteName = currentSite.getSiteName(); + this.siteUrl = currentSite.getURL(); + + this.loaded = true; + + // Load the handlers. + if (this.siteInfo) { + this.user = await CoreUser.getProfile(this.siteInfo.userid); + + this.subscription = CoreUserDelegate.getProfileHandlersFor(this.user).subscribe((handlers) => { + if (!handlers || !this.user) { + return; + } + + this.handlers = []; + handlers.forEach((handler) => { + if (handler.type == CoreUserDelegateService.TYPE_NEW_PAGE) { + this.handlers.push(handler.data); + } + }); + + this.handlersLoaded = CoreUserDelegate.areHandlersLoaded(this.user.id); + }); + + } + } + + /** + * Opens User profile page. + * + * @param event Click event. + */ + async openUserProfile(event: Event): Promise { + if (!this.siteInfo) { + return; + } + + await this.close(event); + + CoreNavigator.navigateToSitePath('user/about', { + params: { + userId: this.siteInfo.userid, + }, + }); + } + + /** + * Opens preferences. + * + * @param event Click event. + */ + async openPreferences(event: Event): Promise { + await this.close(event); + + CoreNavigator.navigateToSitePath('preferences'); + } + + /** + * A handler was clicked. + * + * @param event Click event. + * @param handler Handler that was clicked. + */ + async handlerClicked(event: Event, handler: CoreUserProfileHandlerData): Promise { + if (!this.user) { + return; + } + + await this.close(event); + + handler.action(event, this.user); + } + + /** + * Logout the user. + * + * @param event Click event + */ + async logout(event: Event): Promise { + await this.close(event); + + CoreSites.logout(); + } + + /** + * Show account selector. + * + * @param event Click event + */ + async switchAccounts(event: Event): Promise { + const thisModal = await ModalController.getTop(); + + event.preventDefault(); + event.stopPropagation(); + + const closeAll = await CoreDomUtils.openSideModal({ + component: CoreLoginSitesComponent, + cssClass: 'core-modal-lateral-sm', + }); + + if (closeAll) { + await ModalController.dismiss(undefined, undefined, thisModal.id); + } + } + + /** + * Add account. + * + * @param event Click event + */ + async addAccount(event: Event): Promise { + await this.close(event); + + await CoreLoginHelper.goToAddSite(true, true); + } + + /** + * Close modal. + */ + async close(event: Event): Promise { + event.preventDefault(); + event.stopPropagation(); + + await ModalController.dismiss(); + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.subscription?.unsubscribe(); + } + +} diff --git a/src/core/features/mainmenu/lang.json b/src/core/features/mainmenu/lang.json index a6558e06e..9e9977545 100644 --- a/src/core/features/mainmenu/lang.json +++ b/src/core/features/mainmenu/lang.json @@ -1,7 +1,5 @@ { - "changesite": "Change site", - "help": "Help", "home": "Home", "logout": "Log out", - "website": "Website" + "switchaccount": "Switch account" } diff --git a/src/core/features/mainmenu/mainmenu-lazy.module.ts b/src/core/features/mainmenu/mainmenu-lazy.module.ts index ff9c67189..22f30d18b 100644 --- a/src/core/features/mainmenu/mainmenu-lazy.module.ts +++ b/src/core/features/mainmenu/mainmenu-lazy.module.ts @@ -22,6 +22,7 @@ import { MAIN_MENU_ROUTES } from './mainmenu-routing.module'; import { CoreMainMenuPage } from './pages/menu/menu'; import { CoreMainMenuHomeHandlerService } from './services/handlers/mainmenu'; import { CoreMainMenuProvider } from './services/mainmenu'; +import { CoreMainMenuComponentsModule } from './components/components.module'; function buildRoutes(injector: Injector): Routes { const routes = resolveModuleRoutes(injector, MAIN_MENU_ROUTES); @@ -54,6 +55,7 @@ function buildRoutes(injector: Injector): Routes { @NgModule({ imports: [ CoreSharedModule, + CoreMainMenuComponentsModule, ], declarations: [ CoreMainMenuPage, diff --git a/src/core/features/mainmenu/pages/home/home.html b/src/core/features/mainmenu/pages/home/home.html index 903bcb8ff..e953b24c2 100644 --- a/src/core/features/mainmenu/pages/home/home.html +++ b/src/core/features/mainmenu/pages/home/home.html @@ -8,6 +8,7 @@ + diff --git a/src/core/features/mainmenu/pages/home/home.module.ts b/src/core/features/mainmenu/pages/home/home.module.ts index c60828634..7dcde146c 100644 --- a/src/core/features/mainmenu/pages/home/home.module.ts +++ b/src/core/features/mainmenu/pages/home/home.module.ts @@ -22,6 +22,7 @@ import { CoreMainMenuHomePage } from './home'; import { MAIN_MENU_HOME_ROUTES } from './home-routing.module'; import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; import { CoreMainMenuHomeHandlerService } from '@features/mainmenu/services/handlers/mainmenu'; +import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module'; function buildRoutes(injector: Injector): Routes { const routes = resolveModuleRoutes(injector, MAIN_MENU_HOME_ROUTES); @@ -42,6 +43,7 @@ function buildRoutes(injector: Injector): Routes { @NgModule({ imports: [ CoreSharedModule, + CoreMainMenuComponentsModule, ], providers: [ { provide: ROUTES, multi: true, useFactory: buildRoutes, deps: [Injector] }, diff --git a/src/core/features/mainmenu/pages/menu/menu.html b/src/core/features/mainmenu/pages/menu/menu.html index b888971dc..18a5bb380 100644 --- a/src/core/features/mainmenu/pages/menu/menu.html +++ b/src/core/features/mainmenu/pages/menu/menu.html @@ -4,6 +4,8 @@ [@menuShowHideAnimation]="tabsPlacement == 'side' ? '' : (isMainScreen ? 'visible' : 'hidden')"> + + @@ -18,7 +20,7 @@ - + {{ 'core.more' | translate }} diff --git a/src/core/features/mainmenu/pages/menu/menu.scss b/src/core/features/mainmenu/pages/menu/menu.scss index 7f362ec1d..b359d98f0 100644 --- a/src/core/features/mainmenu/pages/menu/menu.scss +++ b/src/core/features/mainmenu/pages/menu/menu.scss @@ -71,7 +71,7 @@ @include padding(var(--ion-safe-area-top), 0px, var(--ion-safe-area-bottom), var(--ion-safe-area-left)); - ion-tab-button { + ion-tab-button, core-user-menu-button { width: 100%; min-height: var(--menutabbar-size); flex: 0; @@ -83,6 +83,12 @@ } } + core-user-menu-button { + align-items: center; + display: flex; + justify-content: center; + } + .core-network-message { --network-message-height: 16px; position: absolute; diff --git a/src/core/features/mainmenu/pages/more/more.html b/src/core/features/mainmenu/pages/more/more.html index 3f3fcff68..2d111d407 100644 --- a/src/core/features/mainmenu/pages/more/more.html +++ b/src/core/features/mainmenu/pages/more/more.html @@ -4,98 +4,63 @@
-

+

{{ 'core.more' | translate }}

+ + + + - - - - + + + + + + + +

{{ handler.title | translate}}

+
+ + + {{ handler.badgeA11yText | translate: {$a : handler.badge } }} + + + +
+ + + -

{{siteInfo.fullname}}

-

- - -

-

{{ siteUrl }}

+

{{item.label}}

- - - - - - + + -

{{ handler.title | translate}}

-
- - - {{ handler.badgeA11yText | translate: {$a : handler.badge } }} - - - -
- - - - -

{{item.label}}

-
-
- - - -

{{item.label}}

-
-
-
- - - -

{{ 'core.scanqr' | translate }}

+

{{item.label}}

- - - -

{{ 'core.mainmenu.website' | translate }}

-
-
- - - -

{{ 'core.mainmenu.help' | translate }}

-
-
- - - -

{{ 'core.settings.preferences' | translate }}

-
-
- - - -

{{ logoutLabel | translate }}

-
-
- - - - -

{{ 'core.settings.appsettings' | translate }}

-
-
-
-
+ + + + +

{{ 'core.scanqr' | translate }}

+
+
+
+ + + + +

{{ 'core.settings.appsettings' | translate }}

+
+
+
diff --git a/src/core/features/mainmenu/pages/more/more.module.ts b/src/core/features/mainmenu/pages/more/more.module.ts index 1999f1b49..770fa3d9b 100644 --- a/src/core/features/mainmenu/pages/more/more.module.ts +++ b/src/core/features/mainmenu/pages/more/more.module.ts @@ -19,10 +19,12 @@ import { CoreSharedModule } from '@/core/shared.module'; import { CoreMainMenuMorePage } from './more'; import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; import { CoreMainMenuProvider } from '@features/mainmenu/services/mainmenu'; +import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module'; @NgModule({ imports: [ CoreSharedModule, + CoreMainMenuComponentsModule, ], providers: [ { diff --git a/src/core/features/mainmenu/pages/more/more.scss b/src/core/features/mainmenu/pages/more/more.scss index c5b62e00a..9b1b544d2 100644 --- a/src/core/features/mainmenu/pages/more/more.scss +++ b/src/core/features/mainmenu/pages/more/more.scss @@ -17,21 +17,3 @@ ion-item { color: var(--core-more-icon, inherit); } } - -@if ($core-more-hide-siteinfo) { - .core-moremenu-siteinfo { - display: none; - } -} - -@if ($core-more-hide-sitename) { - .core-moremenu-sitename { - display: none; - } -} - -@if ($core-more-hide-siteurl) { - .core-moremenu-siteurl { - display: none; - } -} diff --git a/src/core/features/mainmenu/pages/more/more.ts b/src/core/features/mainmenu/pages/more/more.ts index 733c8a0aa..626a18d27 100644 --- a/src/core/features/mainmenu/pages/more/more.ts +++ b/src/core/features/mainmenu/pages/more/more.ts @@ -17,8 +17,6 @@ import { Subscription } from 'rxjs'; import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; -import { CoreSiteInfo } from '@classes/site'; -import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../services/mainmenu-delegate'; import { CoreMainMenu, CoreMainMenuCustomItem } from '../../services/mainmenu'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -29,7 +27,7 @@ import { CoreTextUtils } from '@services/utils/text'; import { Translate } from '@singletons'; /** - * Page that displays the main menu of the app. + * Page that displays the more page of the app. */ @Component({ selector: 'page-core-mainmenu-more', @@ -39,38 +37,30 @@ import { Translate } from '@singletons'; export class CoreMainMenuMorePage implements OnInit, OnDestroy { handlers?: CoreMainMenuHandlerData[]; - allHandlers?: CoreMainMenuHandlerData[]; handlersLoaded = false; - siteInfo?: CoreSiteInfo; - siteName?: string; - logoutLabel = 'core.mainmenu.changesite'; showScanQR: boolean; - showWeb?: boolean; - showHelp?: boolean; - docsUrl?: string; customItems?: CoreMainMenuCustomItem[]; - siteUrl?: string; - loggedOut = false; + protected allHandlers?: CoreMainMenuHandlerData[]; protected subscription!: Subscription; protected langObserver: CoreEventObserver; protected updateSiteObserver: CoreEventObserver; constructor() { + this.langObserver = CoreEvents.on(CoreEvents.LANGUAGE_CHANGED, this.loadCustomMenuItems.bind(this)); + + this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, async () => { + this.customItems = await CoreMainMenu.getCustomMenuItems(); + }, CoreSites.getCurrentSiteId()); + + this.loadCustomMenuItems(); - this.langObserver = CoreEvents.on(CoreEvents.LANGUAGE_CHANGED, this.loadSiteInfo.bind(this)); - this.updateSiteObserver = CoreEvents.on( - CoreEvents.SITE_UPDATED, - this.loadSiteInfo.bind(this), - CoreSites.getCurrentSiteId(), - ); - this.loadSiteInfo(); this.showScanQR = CoreUtils.canScanQR() && !CoreSites.getCurrentSite()?.isFeatureDisabled('CoreMainMenuDelegate_QrReader'); } /** - * Initialize component. + * @inheritdoc */ ngOnInit(): void { // Load the handlers. @@ -84,7 +74,7 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy { } /** - * Page destroyed. + * @inheritdoc */ ngOnDestroy(): void { window.removeEventListener('resize', this.initHandlers.bind(this)); @@ -113,24 +103,9 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy { } /** - * Load the site info required by the view. + * Load custom menu items. */ - protected async loadSiteInfo(): Promise { - const currentSite = CoreSites.getCurrentSite(); - - if (!currentSite) { - return; - } - - this.siteInfo = currentSite.getInfo(); - this.siteName = currentSite.getSiteName(); - this.siteUrl = currentSite.getURL(); - this.logoutLabel = CoreLoginHelper.getLogoutLabel(currentSite); - this.showWeb = !currentSite.isFeatureDisabled('CoreMainMenuDelegate_website'); - this.showHelp = !currentSite.isFeatureDisabled('CoreMainMenuDelegate_help'); - - this.docsUrl = await currentSite.getDocsUrl(); - + protected async loadCustomMenuItems(): Promise { this.customItems = await CoreMainMenu.getCustomMenuItems(); } @@ -154,13 +129,6 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy { CoreNavigator.navigateToSitePath('viewer/iframe', { params: { title: item.label, url: item.url } }); } - /** - * Open preferences. - */ - openPreferences(): void { - CoreNavigator.navigateToSitePath('preferences'); - } - /** * Open settings. */ @@ -200,12 +168,4 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy { } } - /** - * Logout the user. - */ - logout(): void { - this.loggedOut = true; - CoreSites.logout(); - } - } diff --git a/src/core/features/settings/pages/site/site.html b/src/core/features/settings/pages/site/site.html index b28da0563..af29f5ee0 100644 --- a/src/core/features/settings/pages/site/site.html +++ b/src/core/features/settings/pages/site/site.html @@ -15,18 +15,6 @@ - - -

{{siteInfo!.fullname}}

-

- -

-

{{ siteUrl }}

-
-
- - @@ -36,38 +24,37 @@

{{ handler.title | translate}}

- - - - -

{{ 'core.settings.spaceusage' | translate }}

-

{{ spaceUsage.spaceUsage | coreBytesToSize }}

-
- - - - - - -
- - -

{{ 'core.settings.synchronizenow' | translate }}

-
- - - - - - - - -
-
+ + + +

{{ 'core.settings.spaceusage' | translate }}

+

{{ spaceUsage.spaceUsage | coreBytesToSize }}

+
+ + + + + + +
+ + +

{{ 'core.settings.synchronizenow' | translate }}

+
+ + + + + + + + +
+
diff --git a/src/core/features/settings/pages/site/site.ts b/src/core/features/settings/pages/site/site.ts index be7d0e313..77a366b73 100644 --- a/src/core/features/settings/pages/site/site.ts +++ b/src/core/features/settings/pages/site/site.ts @@ -22,7 +22,6 @@ import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreSettingsHelper, CoreSiteSpaceUsage } from '../../services/settings-helper'; import { CoreApp } from '@services/app'; -import { CoreSiteInfo } from '@classes/site'; import { Translate } from '@singletons'; import { CoreNavigator } from '@services/navigator'; import { CorePageItemsListManager } from '@classes/page-items-list-manager'; @@ -43,9 +42,6 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy { isIOS: boolean; siteId: string; - siteInfo?: CoreSiteInfo; - siteName?: string; - siteUrl?: string; spaceUsage: CoreSiteSpaceUsage = { cacheEntries: 0, spaceUsage: 0, @@ -60,11 +56,9 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy { this.siteId = CoreSites.getCurrentSiteId(); this.handlers = new CoreSettingsSitePreferencesManager(CoreSitePreferencesPage); - this.sitesObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, (data) => { - if (data.siteId == this.siteId) { - this.refreshData(); - } - }); + this.sitesObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { + this.refreshData(); + }, this.siteId); } /** @@ -94,11 +88,6 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy { protected async fetchData(): Promise { this.handlers.setItems(CoreSettingsDelegate.getHandlers()); - const currentSite = CoreSites.getCurrentSite(); - this.siteInfo = currentSite!.getInfo(); - this.siteName = currentSite!.getSiteName(); - this.siteUrl = currentSite!.getURL(); - this.spaceUsage = await CoreSettingsHelper.getSiteSpaceUsage(this.siteId); } @@ -145,7 +134,9 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy { */ async deleteSiteStorage(): Promise { try { - this.spaceUsage = await CoreSettingsHelper.deleteSiteStorage(this.siteName || '', this.siteId); + const siteName = CoreSites.getRequiredCurrentSite().getSiteName(); + + this.spaceUsage = await CoreSettingsHelper.deleteSiteStorage(siteName, this.siteId); } catch { // Ignore cancelled confirmation modal. } diff --git a/src/core/features/settings/pages/space-usage/space-usage.html b/src/core/features/settings/pages/space-usage/space-usage.html index aa17b099a..6d1343a40 100644 --- a/src/core/features/settings/pages/space-usage/space-usage.html +++ b/src/core/features/settings/pages/space-usage/space-usage.html @@ -25,7 +25,7 @@

{{ site.fullName }}

-

{{ site.siteUrl }}

+

{{ site.siteUrlWithoutProtocol }}

{{ site.spaceUsage | coreBytesToSize }} diff --git a/src/core/features/settings/pages/synchronization/synchronization.html b/src/core/features/settings/pages/synchronization/synchronization.html index 625d67b88..31fe30857 100644 --- a/src/core/features/settings/pages/synchronization/synchronization.html +++ b/src/core/features/settings/pages/synchronization/synchronization.html @@ -37,7 +37,7 @@

{{ site.fullName }}

-

{{ site.siteUrl }}

+

{{ site.siteUrlWithoutProtocol }}

+ diff --git a/src/core/features/tag/pages/search/search.html b/src/core/features/tag/pages/search/search.html index 5afa3e371..1f6ff37a9 100644 --- a/src/core/features/tag/pages/search/search.html +++ b/src/core/features/tag/pages/search/search.html @@ -4,6 +4,9 @@

{{ 'core.tag.searchtags' | translate }}

+ + + diff --git a/src/core/features/tag/pages/search/search.page.module.ts b/src/core/features/tag/pages/search/search.page.module.ts index 3da4821d2..5519cb3ac 100644 --- a/src/core/features/tag/pages/search/search.page.module.ts +++ b/src/core/features/tag/pages/search/search.page.module.ts @@ -19,6 +19,7 @@ import { CoreSharedModule } from '@/core/shared.module'; import { CoreSearchComponentsModule } from '@features/search/components/components.module'; import { CoreTagSearchPage } from './search.page'; +import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module'; const routes: Routes = [ { @@ -34,6 +35,7 @@ const routes: Routes = [ RouterModule.forChild(routes), CoreSharedModule, CoreSearchComponentsModule, + CoreMainMenuComponentsModule, ], exports: [RouterModule], }) diff --git a/src/core/features/tag/tag-lazy.module.ts b/src/core/features/tag/tag-lazy.module.ts index e0c6f7b0e..6ced82dfe 100644 --- a/src/core/features/tag/tag-lazy.module.ts +++ b/src/core/features/tag/tag-lazy.module.ts @@ -35,7 +35,7 @@ function buildRoutes(injector: Injector): Routes { data: { mainMenuTabRoot: CoreTagMainMenuHandlerService.PAGE_NAME, }, - loadChildren: () => import('@features/tag//pages/search/search.page.module').then(m => m.CoreTagSearchPageModule), + loadChildren: () => import('@features/tag/pages/search/search.page.module').then(m => m.CoreTagSearchPageModule), }, CoreTagIndexAreaRoute, ...buildTabMainRoutes(injector, { diff --git a/src/core/features/user/lang.json b/src/core/features/user/lang.json index 610e0fae4..61d41e287 100644 --- a/src/core/features/user/lang.json +++ b/src/core/features/user/lang.json @@ -1,5 +1,6 @@ { "address": "Address", + "account": "Account", "city": "City/town", "contact": "Contact", "country": "Country", @@ -19,6 +20,7 @@ "participants": "Participants", "phone1": "Phone", "phone2": "Mobile phone", + "profile": "Profile", "roles": "Roles", "sendemail": "Email", "student": "Student", diff --git a/src/core/features/user/pages/about/about.html b/src/core/features/user/pages/about/about.html index fb08137f1..03abbef38 100644 --- a/src/core/features/user/pages/about/about.html +++ b/src/core/features/user/pages/about/about.html @@ -3,7 +3,7 @@ -

{{ title }}

+

{{ 'core.user.profile' | translate }}

@@ -12,39 +12,57 @@ + + -

{{ 'core.user.contact' | translate}}

+ + +

{{ 'core.user.contact' | translate}}

+
+

{{ 'core.user.email' | translate }}

-

- {{ user.email }} -

+

+ {{ user.email }} +

{{ 'core.user.phone1' | translate}}

- {{ user.phone1 }} -

+ {{ user.phone1 }} +

{{ 'core.user.phone2' | translate}}

- {{ user.phone2 }} -

+ {{ user.phone2 }} +

{{ 'core.user.address' | translate}}

- {{ formattedAddress }} -

+ {{ formattedAddress }} +

@@ -61,13 +79,17 @@
-

{{ 'core.userdetails' | translate}}

+ + +

{{ 'core.userdetails' | translate}}

+
+

{{ 'core.user.webpage' | translate}}

- {{ user.url }} -

+ {{ user.url }} +

@@ -81,11 +103,17 @@
-

{{ 'core.user.description' | translate}}

+ + +

{{ 'core.user.description' | translate}}

+
+
-

-

+

+ + +

diff --git a/src/core/features/user/pages/about/about.page.ts b/src/core/features/user/pages/about/about.page.ts index 2c3e5808f..33bbdb8f9 100644 --- a/src/core/features/user/pages/about/about.page.ts +++ b/src/core/features/user/pages/about/about.page.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { SafeUrl } from '@angular/platform-browser'; import { IonRefresher } from '@ionic/angular'; @@ -20,10 +20,15 @@ import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; -import { CoreEvents } from '@singletons/events'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreUser, CoreUserProfile, CoreUserProvider } from '@features/user/services/user'; import { CoreUserHelper } from '@features/user/services/user-helper'; import { CoreNavigator } from '@services/navigator'; +import { CoreIonLoadingElement } from '@classes/ion-loading'; +import { CoreSite } from '@classes/site'; +import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuploader-helper'; +import { CoreMimetypeUtils } from '@services/utils/mimetype'; +import { Translate } from '@singletons'; /** * Page that displays info about a user. @@ -31,11 +36,9 @@ import { CoreNavigator } from '@services/navigator'; @Component({ selector: 'page-core-user-about', templateUrl: 'about.html', + styleUrls: ['about.scss'], }) -export class CoreUserAboutPage implements OnInit { - - protected userId!: number; - protected siteId: string; +export class CoreUserAboutPage implements OnInit, OnDestroy { courseId!: number; userLoaded = false; @@ -45,20 +48,46 @@ export class CoreUserAboutPage implements OnInit { title?: string; formattedAddress?: string; encodedAddress?: SafeUrl; + canChangeProfilePicture = false; + + protected userId!: number; + protected site!: CoreSite; + protected obsProfileRefreshed?: CoreEventObserver; constructor() { - this.siteId = CoreSites.getCurrentSiteId(); + try { + this.site = CoreSites.getRequiredCurrentSite(); + } catch (error) { + CoreDomUtils.showErrorModal(error); + CoreNavigator.back(); + + return; + } + + this.obsProfileRefreshed = CoreEvents.on(CoreUserProvider.PROFILE_REFRESHED, (data) => { + if (!this.user || !data.user) { + return; + } + + this.user.email = data.user.email; + this.user.address = CoreUserHelper.formatAddress('', data.user.city, data.user.country); + }, CoreSites.getCurrentSiteId()); } /** - * On init. - * - * @return Promise resolved when done. + * @inheritdoc */ async ngOnInit(): Promise { this.userId = CoreNavigator.getRouteNumberParam('userId') || 0; this.courseId = CoreNavigator.getRouteNumberParam('courseId') || 0; + // Allow to change the profile image only in the app profile page. + this.canChangeProfilePicture = + !this.courseId && + this.userId == this.site.getUserId() && + this.site.canUploadFiles() && + !CoreUser.isUpdatePictureDisabledInSite(this.site); + this.fetchUser().finally(() => { this.userLoaded = true; }); @@ -83,11 +112,85 @@ export class CoreUserAboutPage implements OnInit { this.user = user; this.title = user.fullname; + + this.user.address = CoreUserHelper.formatAddress('', user.city, user.country); + + await this.checkUserImageUpdated(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'core.user.errorloaduser', true); } } + /** + * Check if current user image has changed. + * + * @return Promise resolved when done. + */ + protected async checkUserImageUpdated(): Promise { + if (!this.site || !this.site.getInfo() || !this.user) { + return; + } + + if (this.userId != this.site.getUserId() || !this.isUserAvatarDirty()) { + // Not current user or hasn't changed. + return; + } + + // The current user image received is different than the one stored in site info. Assume the image was updated. + // Update the site info to get the right avatar in there. + try { + await CoreSites.updateSiteInfo(this.site.getId()); + } catch { + // Cannot update site info. Assume the profile image is the right one. + CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, { + userId: this.userId, + picture: this.user.profileimageurl, + }, this.site.getId()); + } + + if (this.isUserAvatarDirty()) { + // The image is still different, this means that the good one is the one in site info. + await this.refreshUser(); + } else { + // Now they're the same, send event to use the right avatar in the rest of the app. + CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, { + userId: this.userId, + picture: this.user.profileimageurl, + }, this.site.getId()); + } + } + + /** + * Opens dialog to change profile picture. + */ + async changeProfilePicture(): Promise { + const maxSize = -1; + const title = Translate.instant('core.user.newpicture'); + const mimetypes = CoreMimetypeUtils.getGroupMimeInfo('image', 'mimetypes'); + let modal: CoreIonLoadingElement | undefined; + + try { + const result = await CoreFileUploaderHelper.selectAndUploadFile(maxSize, title, mimetypes); + + modal = await CoreDomUtils.showModalLoading('core.sending', true); + + const profileImageURL = await CoreUser.changeProfilePicture(result.itemid, this.userId, this.site.getId()); + + CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, { + userId: this.userId, + picture: profileImageURL, + }, this.site.getId()); + + CoreSites.updateSiteInfo(this.site.getId()); + + this.refreshUser(); + } catch (error) { + CoreDomUtils.showErrorModal(error); + } finally { + modal?.dismiss(); + } + } + /** * Refresh the user data. * @@ -106,8 +209,52 @@ export class CoreUserAboutPage implements OnInit { courseId: this.courseId, userId: this.userId, user: this.user, - }, this.siteId); + }, this.site.getId()); } } + /** + * Check whether the user avatar is not up to date with site info. + * + * @return Whether the user avatar differs from site info cache. + */ + protected isUserAvatarDirty(): boolean { + if (!this.user || !this.site) { + return false; + } + + const courseAvatarUrl = this.normalizeAvatarUrl(this.user.profileimageurl); + const siteAvatarUrl = this.normalizeAvatarUrl(this.site.getInfo()?.userpictureurl); + + return courseAvatarUrl !== siteAvatarUrl; + } + + /** + * Normalize an avatar url regardless of theme. + * + * Given that the default image is the only one that can be changed per theme, any other url will stay the same. Note that + * the values returned by this function may not be valid urls, given that they are intended for string comparison. + * + * @param avatarUrl Avatar url. + * @return Normalized avatar string (may not be a valid url). + */ + protected normalizeAvatarUrl(avatarUrl?: string): string { + if (!avatarUrl) { + return 'undefined'; + } + + if (avatarUrl.startsWith(`${this.site?.siteUrl}/theme/image.php`)) { + return 'default'; + } + + return avatarUrl; + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.obsProfileRefreshed?.off(); + } + } diff --git a/src/core/features/user/pages/about/about.scss b/src/core/features/user/pages/about/about.scss new file mode 100644 index 000000000..c5c1284cb --- /dev/null +++ b/src/core/features/user/pages/about/about.scss @@ -0,0 +1,44 @@ +:host { + + .core-user-profile-maininfo::part(native) { + flex-direction: column; + } + ::ng-deep { + core-user-avatar { + display: block; + --core-avatar-size: var(--core-large-avatar-size); + height: calc(var(--core-avatar-size) + 16px); + + img { + margin: 8px auto; + } + + .contact-status { + width: 24px !important; + height: 24px !important; + right: calc(50% - 12px - var(--core-avatar-size) / 2) !important; + } + + .edit-avatar { + position: absolute; + right: calc(50% - 15px - var(--core-avatar-size) / 2); + bottom: -12px; + + :host-context([dir="rtl"]) & { + left: 0; + right: unset; + } + &::part(native) { + border-radius: 50%; + background: var(--ion-item-background); + } + } + } + } + +} + +:host-context([dir="rtl"]) ::ng-deep core-user-avatar .edit-avatar { + left: -24px; + right: unset; +} diff --git a/src/core/features/user/pages/profile/profile.html b/src/core/features/user/pages/profile/profile.html index 2bedd26e5..ce58f5ae1 100644 --- a/src/core/features/user/pages/profile/profile.html +++ b/src/core/features/user/pages/profile/profile.html @@ -14,20 +14,12 @@ @@ -86,8 +88,7 @@ - + diff --git a/src/core/features/user/pages/profile/profile.page.ts b/src/core/features/user/pages/profile/profile.page.ts index 6e0f72013..d8ffe46e4 100644 --- a/src/core/features/user/pages/profile/profile.page.ts +++ b/src/core/features/user/pages/profile/profile.page.ts @@ -19,8 +19,6 @@ import { Subscription } from 'rxjs'; import { CoreSite } from '@classes/site'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreMimetypeUtils } from '@services/utils/mimetype'; -import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreUser, @@ -29,8 +27,6 @@ import { } from '@features/user/services/user'; import { CoreUserHelper } from '@features/user/services/user-helper'; import { CoreUserDelegate, CoreUserDelegateService, CoreUserProfileHandlerData } from '@features/user/services/user-delegate'; -import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuploader-helper'; -import { CoreIonLoadingElement } from '@classes/ion-loading'; import { CoreUtils } from '@services/utils/utils'; import { CoreNavigator } from '@services/navigator'; import { CoreCourses } from '@features/courses/services/courses'; @@ -54,7 +50,6 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { title?: string; isDeleted = false; isEnrolled = true; - canChangeProfilePicture = false; rolesFormatted?: string; actionHandlers: CoreUserProfileHandlerData[] = []; newPageHandlers: CoreUserProfileHandlerData[] = []; @@ -72,7 +67,7 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { } /** - * On init. + * @inheritdoc */ async ngOnInit(): Promise { try { @@ -91,13 +86,6 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { this.courseId = undefined; } - // Allow to change the profile image only in the app profile page. - this.canChangeProfilePicture = - !this.courseId && - this.userId == this.site.getUserId() && - this.site.canUploadFiles() && - !CoreUser.isUpdatePictureDisabledInSite(this.site); - try { await this.fetchUser(); @@ -154,84 +142,12 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { this.isLoadingHandlers = !CoreUserDelegate.areHandlersLoaded(user.id); }); - await this.checkUserImageUpdated(); - } catch (error) { // Error is null for deleted users, do not show the modal. CoreDomUtils.showErrorModal(error); } } - /** - * Check if current user image has changed. - * - * @return Promise resolved when done. - */ - protected async checkUserImageUpdated(): Promise { - if (!this.site || !this.site.getInfo() || !this.user) { - return; - } - - if (this.userId != this.site.getUserId() || !this.isUserAvatarDirty()) { - // Not current user or hasn't changed. - return; - } - - // The current user image received is different than the one stored in site info. Assume the image was updated. - // Update the site info to get the right avatar in there. - try { - await CoreSites.updateSiteInfo(this.site.getId()); - } catch { - // Cannot update site info. Assume the profile image is the right one. - CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, { - userId: this.userId, - picture: this.user.profileimageurl, - }, this.site.getId()); - } - - if (this.isUserAvatarDirty()) { - // The image is still different, this means that the good one is the one in site info. - await this.refreshUser(); - } else { - // Now they're the same, send event to use the right avatar in the rest of the app. - CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, { - userId: this.userId, - picture: this.user.profileimageurl, - }, this.site.getId()); - } - } - - /** - * Opens dialog to change profile picture. - */ - async changeProfilePicture(): Promise { - const maxSize = -1; - const title = Translate.instant('core.user.newpicture'); - const mimetypes = CoreMimetypeUtils.getGroupMimeInfo('image', 'mimetypes'); - let modal: CoreIonLoadingElement | undefined; - - try { - const result = await CoreFileUploaderHelper.selectAndUploadFile(maxSize, title, mimetypes); - - modal = await CoreDomUtils.showModalLoading('core.sending', true); - - const profileImageURL = await CoreUser.changeProfilePicture(result.itemid, this.userId, this.site.getId()); - - CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, { - userId: this.userId, - picture: profileImageURL, - }, this.site.getId()); - - CoreSites.updateSiteInfo(this.site.getId()); - - this.refreshUser(); - } catch (error) { - CoreDomUtils.showErrorModal(error); - } finally { - modal?.dismiss(); - } - } - /** * Refresh the user. * @@ -285,48 +201,11 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { } /** - * Page destroyed. + * @inheritdoc */ ngOnDestroy(): void { this.subscription?.unsubscribe(); this.obsProfileRefreshed.off(); } - /** - * Check whether the user avatar is not up to date with site info. - * - * @return Whether the user avatar differs from site info cache. - */ - private isUserAvatarDirty(): boolean { - if (!this.user || !this.site) { - return false; - } - - const courseAvatarUrl = this.normalizeAvatarUrl(this.user.profileimageurl); - const siteAvatarUrl = this.normalizeAvatarUrl(this.site.getInfo()?.userpictureurl); - - return courseAvatarUrl !== siteAvatarUrl; - } - - /** - * Normalize an avatar url regardless of theme. - * - * Given that the default image is the only one that can be changed per theme, any other url will stay the same. Note that - * the values returned by this function may not be valid urls, given that they are intended for string comparison. - * - * @param avatarUrl Avatar url. - * @return Normalized avatar string (may not be a valid url). - */ - private normalizeAvatarUrl(avatarUrl?: string): string { - if (!avatarUrl) { - return 'undefined'; - } - - if (avatarUrl.startsWith(`${this.site?.siteUrl}/theme/image.php`)) { - return 'default'; - } - - return avatarUrl; - } - } diff --git a/src/core/features/user/pages/profile/profile.scss b/src/core/features/user/pages/profile/profile.scss index c5c1284cb..41723e265 100644 --- a/src/core/features/user/pages/profile/profile.scss +++ b/src/core/features/user/pages/profile/profile.scss @@ -18,21 +18,6 @@ height: 24px !important; right: calc(50% - 12px - var(--core-avatar-size) / 2) !important; } - - .edit-avatar { - position: absolute; - right: calc(50% - 15px - var(--core-avatar-size) / 2); - bottom: -12px; - - :host-context([dir="rtl"]) & { - left: 0; - right: unset; - } - &::part(native) { - border-radius: 50%; - background: var(--ion-item-background); - } - } } } diff --git a/src/core/features/user/services/user-delegate.ts b/src/core/features/user/services/user-delegate.ts index 5901fa17d..b16a36787 100644 --- a/src/core/features/user/services/user-delegate.ts +++ b/src/core/features/user/services/user-delegate.ts @@ -111,6 +111,26 @@ export interface CoreUserProfileHandlerData { */ spinner?: boolean; + /** + * If the handler has badge to show or not. Only for TYPE_NEW_PAGE. + */ + showBadge?: boolean; + + /** + * Text to display on the badge. Only used if showBadge is true and only for TYPE_NEW_PAGE. + */ + badge?: string; + + /** + * Accessibility text to add on the badge. Only used if showBadge is true and only for TYPE_NEW_PAGE. + */ + badgeA11yText?: string; + + /** + * If true, the badge number is being loaded. Only used if showBadge is true and only for TYPE_NEW_PAGE. + */ + loading?: boolean; + /** * Action to do when clicked. * diff --git a/src/core/lang.json b/src/core/lang.json index f067e4b9e..2a7408ed0 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -185,7 +185,7 @@ "mod_wiki": "Wiki", "mod_workshop": "Workshop", "moduleintro": "Description", - "more": "more", + "more": "More", "mygroups": "My groups", "name": "Name", "needhelp": "Need help?", diff --git a/src/core/services/app.ts b/src/core/services/app.ts index 4c5cfc1e2..b3419ce9e 100644 --- a/src/core/services/app.ts +++ b/src/core/services/app.ts @@ -606,7 +606,7 @@ export class CoreAppProvider { }; localStorage.setItem('CoreRedirect', JSON.stringify(redirect)); - } catch (ex) { + } catch { // Ignore errors. } } diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index 702ca7a50..abbcc0de6 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -428,7 +428,7 @@ export class CoreSitesProvider { const result = this.isValidMoodleVersion(info); if (result != CoreSitesProvider.VALID_VERSION) { - return this.treatInvalidAppVersion(result, siteUrl); + return this.treatInvalidAppVersion(result); } const siteId = this.createSiteID(info.siteurl, info.username); @@ -492,7 +492,7 @@ export class CoreSitesProvider { } catch (error) { // Error invaliddevice is returned by Workplace server meaning the same as connecttoworkplaceapp. if (error && error.errorcode == 'invaliddevice') { - return this.treatInvalidAppVersion(CoreSitesProvider.WORKPLACE_APP, siteUrl); + return this.treatInvalidAppVersion(CoreSitesProvider.WORKPLACE_APP); } throw error; @@ -503,14 +503,13 @@ export class CoreSitesProvider { * Having the result of isValidMoodleVersion, it treats the error message to be shown. * * @param result Result returned by isValidMoodleVersion function. - * @param siteUrl The site url. * @param siteId If site is already added, it will invalidate the token. * @return A promise rejected with the error info. */ - protected async treatInvalidAppVersion(result: number, siteUrl: string, siteId?: string): Promise { + protected async treatInvalidAppVersion(result: number, siteId?: string): Promise { let errorCode: string | undefined; let errorKey: string | undefined; - let translateParams; + let translateParams = {}; switch (result) { case CoreSitesProvider.MOODLE_APP: @@ -528,7 +527,7 @@ export class CoreSitesProvider { } if (siteId) { - await this.setSiteLoggedOut(siteId, true); + await this.setSiteLoggedOut(siteId); } throw new CoreSiteError({ @@ -746,7 +745,7 @@ export class CoreSitesProvider { if (siteId) { // Logout the currentSite and expire the token. this.logout(); - this.setSiteLoggedOut(siteId, true); + this.setSiteLoggedOut(siteId); } }); @@ -1053,8 +1052,10 @@ export class CoreSitesProvider { * @param siteId The site ID. If not defined, current site (if available). * @return Promise resolved with site home ID. */ - getSiteHomeId(siteId?: string): Promise { - return this.getSite(siteId).then((site) => site.getSiteHomeId()); + async getSiteHomeId(siteId?: string): Promise { + const site = await this.getSite(siteId); + + return site.getSiteHomeId(); } /** @@ -1075,6 +1076,7 @@ export class CoreSitesProvider { const basicInfo: CoreSiteBasicInfo = { id: site.id, siteUrl: site.siteUrl, + siteUrlWithoutProtocol: site.siteUrl.replace(/^https?:\/\//, '').toLowerCase(), fullName: siteInfo?.fullname, siteName: CoreConstants.CONFIG.sitename == '' ? siteInfo?.sitename: CoreConstants.CONFIG.sitename, avatar: siteInfo?.userpictureurl, @@ -1096,12 +1098,10 @@ export class CoreSitesProvider { async getSortedSites(ids?: string[]): Promise { const sites = await this.getSites(ids); - // Sort sites by url and ful lname. + // Sort sites by url and fullname. sites.sort((a, b) => { // First compare by site url without the protocol. - const urlA = a.siteUrl.replace(/^https?:\/\//, '').toLowerCase(); - const urlB = b.siteUrl.replace(/^https?:\/\//, '').toLowerCase(); - const compare = urlA.localeCompare(urlB); + const compare = a.siteUrlWithoutProtocol.localeCompare(b.siteUrlWithoutProtocol); if (compare !== 0) { return compare; @@ -1191,7 +1191,7 @@ export class CoreSitesProvider { this.currentSite = undefined; if (siteConfig && siteConfig.tool_mobile_forcelogout == '1') { - promises.push(this.setSiteLoggedOut(siteId, true)); + promises.push(this.setSiteLoggedOut(siteId)); } promises.push(this.removeStoredCurrentSite()); @@ -1221,34 +1221,24 @@ export class CoreSitesProvider { this.logger.debug(`Restore session in site ${siteId}`); await this.loadSite(siteId); - } catch (err) { + } catch { // No current session. } } /** - * Mark or unmark a site as logged out so the user needs to authenticate again. + * Mark a site as logged out so the user needs to authenticate again. * * @param siteId ID of the site. - * @param loggedOut True to set the site as logged out, false otherwise. * @return Promise resolved when done. */ - async setSiteLoggedOut(siteId: string, loggedOut: boolean): Promise { + protected async setSiteLoggedOut(siteId: string): Promise { const db = await this.appDB; const site = await this.getSite(siteId); - const newValues: Partial = { - loggedOut: loggedOut ? 1 : 0, - }; - if (loggedOut) { - // Erase the token for security. - newValues.token = ''; - site.token = ''; - } + site.setLoggedOut(true); - site.setLoggedOut(loggedOut); - - await db.updateRecords(SITES_TABLE_NAME, newValues, { id: siteId }); + await db.updateRecords(SITES_TABLE_NAME, { loggedOut: 1 }, { id: siteId }); } /** @@ -1315,7 +1305,7 @@ export class CoreSitesProvider { const versionCheck = this.isValidMoodleVersion(info); if (versionCheck != CoreSitesProvider.VALID_VERSION) { // The Moodle version is not supported, reject. - return this.treatInvalidAppVersion(versionCheck, site.getURL(), site.getId()); + return this.treatInvalidAppVersion(versionCheck, site.getId()); } // Try to get the site config. @@ -1344,7 +1334,7 @@ export class CoreSitesProvider { } finally { CoreEvents.trigger(CoreEvents.SITE_UPDATED, info, siteId); } - } catch (error) { + } catch { // Ignore that we cannot fetch site info. Probably the auth token is invalid. } } @@ -1417,7 +1407,7 @@ export class CoreSitesProvider { await Promise.all(promises); return ids; - } catch (error) { + } catch { // Shouldn't happen. return []; } @@ -1475,8 +1465,10 @@ export class CoreSitesProvider { * @param siteId The site ID. If not defined, current site (if available). * @return Promise resolved with true if disabled. */ - isFeatureDisabled(name: string, siteId?: string): Promise { - return this.getSite(siteId).then((site) => site.isFeatureDisabled(name)); + async isFeatureDisabled(name: string, siteId?: string): Promise { + const site = await this.getSite(siteId); + + return site.isFeatureDisabled(name); } /** @@ -1763,40 +1755,14 @@ export type CoreSiteUserTokenResponse = { * Site's basic info. */ export type CoreSiteBasicInfo = { - /** - * Site ID. - */ - id: string; - - /** - * Site URL. - */ - siteUrl: string; - - /** - * User's full name. - */ - fullName?: string; - - /** - * Site's name. - */ - siteName?: string; - - /** - * User's avatar. - */ - avatar?: string; - - /** - * Badge to display in the site. - */ - badge?: number; - - /** - * Site home ID. - */ - siteHomeId?: number; + id: string; // Site ID. + siteUrl: string; // Site URL. + siteUrlWithoutProtocol: string; // Site URL without protocol. + fullName?: string; // User's full name. + siteName?: string; // Site's name. + avatar?: string; // User's avatar. + badge?: number; // Badge to display in the site. + siteHomeId?: number; // Site home ID. }; /** diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 093e072a9..e4b6deb5a 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -1677,13 +1677,13 @@ export class CoreDomUtilsProvider { modalOptions: ModalOptions, ): Promise { - modalOptions = Object.assign(modalOptions, { + modalOptions = Object.assign({ cssClass: 'core-modal-lateral', showBackdrop: true, backdropDismiss: true, enterAnimation: CoreModalLateralTransitionEnter, leaveAnimation: CoreModalLateralTransitionLeave, - }); + }, modalOptions); return await this.openModal(modalOptions); } diff --git a/src/theme/globals.variables.scss b/src/theme/globals.variables.scss index e2a39b886..3a1dbff38 100644 --- a/src/theme/globals.variables.scss +++ b/src/theme/globals.variables.scss @@ -102,6 +102,8 @@ $screen-breakpoints: ( xl: 1200px ) !default; +$modal-lateral-width: 360px; + $core-course-image-background: #81ecec, #74b9ff, #a29bfe, #dfe6e9, #00b894, #0984e3, #b2bec3, #fdcb6e, #fd79a8, #6c5ce7 !default; $core-dd-question-colors: #FFFFFF, #B0C4DE, #DCDCDC, #D8BFD8, #87CEFA, #DAA520, #FFD700, #F0E68C !default; $core-text-hightlight-background-color: lighten($blue, 40%) !default; @@ -127,7 +129,15 @@ $core-login-loading-color-dark: $text-color-dark !default; $core-login-hide-forgot-password: false !default; $core-login-hide-need-help: false !default; -// Configuration options for more page. +// Configuration options for more page. (deprecated on 4.0) $core-more-hide-siteinfo: false !default; $core-more-hide-sitename: false !default; $core-more-hide-siteurl: false !default; + +// Configuration options for user page. +$core-user-hide-siteinfo: $core-more-hide-siteinfo !default; +$core-user-hide-sitename: $core-more-hide-sitename !default; +$core-user-hide-siteurl: $core-more-hide-siteurl !default; + + + diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index c040c18d3..c74dae05a 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -508,7 +508,7 @@ body.core-iframe-fullscreen ion-router-outlet { z-index: 100000 !important; } -@media only screen and (min-height: 400px) and (min-width: 300px) { +@media only screen and (min-height: 400px) and (min-width: #{$modal-lateral-width}) { .core-modal-lateral { --ion-safe-area-left: 0px; --ion-safe-area-right: 0px; @@ -519,7 +519,7 @@ body.core-iframe-fullscreen ion-router-outlet { display: block; height: 100% !important; width: auto; - min-width: 300px; + min-width: #{$modal-lateral-width}; box-shadow: 0 28px 48px rgba(0, 0, 0, 0.4); } ion-backdrop { @@ -528,6 +528,29 @@ body.core-iframe-fullscreen ion-router-outlet { } } +@each $breakpoint, $width in $screen-breakpoints { + @media only screen and (min-height: 400px) and (min-width: #{$width}) { + .core-modal-lateral-#{$breakpoint} { + --ion-safe-area-left: 0px; + --ion-safe-area-right: 0px; + + .modal-wrapper { + position: absolute; + @include position(0 !important, 0 !important, 0 !important, unset !important); + display: block; + height: 100% !important; + width: auto; + min-width: #{$width}; + box-shadow: 0 28px 48px rgba(0, 0, 0, 0.4); + } + ion-backdrop { + visibility: visible; + } + } + } + +} + // Hidden submit button. .core-submit-hidden-enter { position: absolute; @@ -655,12 +678,13 @@ ion-card ion-item:only-child { ion-toolbar h1 img.core-bar-button-image, ion-toolbar h1 .core-bar-button-image img { - padding: 0; + padding: 4px; width: var(--core-header-toolbar-button-image-size); height: var(--core-header-toolbar-button-image-size); max-width: var(--core-header-toolbar-button-image-size); max-height: var(--core-header-toolbar-button-image-size); border-radius: 50%; + display: block; } // Action sheet. @@ -1038,6 +1062,10 @@ ion-item.item-multiple-inputs.only-links { } } +a { + cursor: pointer; +} + // Case with ion-input + ion-select inside. ion-item.item-input.item-multiple-inputs { .flex-row {