forked from CIT/Vmeda.Online
		
	MOBILE-3807 sites: New change user feature
This commit is contained in:
		
							parent
							
								
									357d23c082
								
							
						
					
					
						commit
						03b7660d68
					
				@ -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,9 +1926,9 @@
 | 
			
		||||
  "core.login.youcanstillconnectwithcredentials": "local_moodlemobileapp",
 | 
			
		||||
  "core.login.yourenteredsite": "local_moodlemobileapp",
 | 
			
		||||
  "core.lostconnection": "local_moodlemobileapp",
 | 
			
		||||
  "core.mainmenu.changesite": "local_moodlemobileapp",
 | 
			
		||||
  "core.mainmenu.home": "moodle",
 | 
			
		||||
  "core.mainmenu.logout": "moodle",
 | 
			
		||||
  "core.mainmenu.switchaccount": "local_moodlemobileapp",
 | 
			
		||||
  "core.maxfilesize": "moodle",
 | 
			
		||||
  "core.maxsizeandattachments": "moodle",
 | 
			
		||||
  "core.min": "moodle",
 | 
			
		||||
@ -2230,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",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										36
									
								
								src/core/features/login/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/core/features/login/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -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 {}
 | 
			
		||||
							
								
								
									
										92
									
								
								src/core/features/login/components/sites/sites.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/core/features/login/components/sites/sites.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,92 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-button fill="clear" (click)="close($event)" [attr.aria-label]="'core.back' | translate">
 | 
			
		||||
                <ion-icon name="arrow-back" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
 | 
			
		||||
        <h1>{{ 'core.mainmenu.switchaccount' | translate }}</h1>
 | 
			
		||||
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <ion-button fill="clear" *ngIf="accountsList.count > 1" (click)="toggleDelete()"
 | 
			
		||||
                [attr.aria-label]="'core.login.toggleremove' | translate">
 | 
			
		||||
                <ion-icon slot="icon-only" name="fas-pen" aria-hidden="true"></ion-icon>
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <core-loading [hideUntil]="loaded">
 | 
			
		||||
        <ion-list>
 | 
			
		||||
            <ng-container *ngIf="accountsList.currentSite">
 | 
			
		||||
                <ion-item-divider sticky="true">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>
 | 
			
		||||
                            <core-format-text [text]="accountsList.currentSite.siteName" clean="true"
 | 
			
		||||
                                [siteId]="accountsList.currentSite.id"></core-format-text>
 | 
			
		||||
                        </h2>
 | 
			
		||||
                        <p><a [href]="accountsList.currentSite.siteUrl" core-link autoLogin="yes">{{ accountsList.currentSite.siteUrl }}</a>
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item-divider>
 | 
			
		||||
 | 
			
		||||
                <ion-item detail="false" class="item-current">
 | 
			
		||||
                    <ion-avatar slot="start">
 | 
			
		||||
                        <img [src]="accountsList.currentSite.avatar" core-external-content [siteId]="accountsList.currentSite.id"
 | 
			
		||||
                            alt="{{ 'core.pictureof' | translate:{$a: accountsList.currentSite.fullName} }}"
 | 
			
		||||
                            onError="this.src='assets/img/user-avatar.png'">
 | 
			
		||||
                    </ion-avatar>
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <p class="item-heading">{{accountsList.currentSite.fullName}}</p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-icon color="success" name="fas-check"></ion-icon>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <ng-container *ngTemplateOutlet="siteList; context: {sites: accountsList.sameSite}"></ng-container>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
 | 
			
		||||
            <ng-container *ngFor="let sites of accountsList.otherSites">
 | 
			
		||||
                <ion-item-divider sticky="true" *ngIf="sites[0]">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>
 | 
			
		||||
                            <core-format-text [text]="sites[0].siteName" clean="true" [siteId]="sites[0].id"></core-format-text>
 | 
			
		||||
                        </h2>
 | 
			
		||||
                        <p><a [href]="sites[0].siteUrl" core-link autoLogin="no">{{ sites[0].siteUrl }}</a></p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item-divider>
 | 
			
		||||
 | 
			
		||||
                <ng-container *ngTemplateOutlet="siteList; context: {sites: sites}"></ng-container>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
 | 
			
		||||
        </ion-list>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
    <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end">
 | 
			
		||||
        <ion-fab-button (click)="add($event)" [attr.aria-label]="'core.login.add' | translate">
 | 
			
		||||
            <ion-icon name="fas-plus" aria-hidden="true"></ion-icon>
 | 
			
		||||
            <span class="sr-only">{{ 'core.login.add' | translate }}</span>
 | 
			
		||||
        </ion-fab-button>
 | 
			
		||||
    </ion-fab>
 | 
			
		||||
</ion-content>
 | 
			
		||||
 | 
			
		||||
<!-- Template to render a list of sites. -->
 | 
			
		||||
<ng-template #siteList let-sites="sites">
 | 
			
		||||
    <ion-item button *ngFor="let site of sites" (click)="login($event, site.id)" detail="true">
 | 
			
		||||
        <ion-avatar slot="start">
 | 
			
		||||
            <img [src]="site.avatar" core-external-content [siteId]="site.id" alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}"
 | 
			
		||||
                onError="this.src='assets/img/user-avatar.png'">
 | 
			
		||||
        </ion-avatar>
 | 
			
		||||
        <ion-label>
 | 
			
		||||
            <p class="item-heading">{{site.fullName}}</p>
 | 
			
		||||
        </ion-label>
 | 
			
		||||
        <ion-badge slot="end" *ngIf="!showDelete && site.badge" @coreShowHideAnimation>
 | 
			
		||||
            <span aria-hidden="true">{{site.badge}}</span>
 | 
			
		||||
            <span class="sr-only">{{ 'core.login.sitebadgedescription' | translate:{ count: site.badge }
 | 
			
		||||
                }}</span>
 | 
			
		||||
        </ion-badge>
 | 
			
		||||
        <ion-button *ngIf="showDelete" slot="end" fill="clear" color="danger" (click)="deleteSite($event, site)"
 | 
			
		||||
            [attr.aria-label]="'core.login.removeaccount' | translate" [@coreSlideInOut]="'fromRight'">
 | 
			
		||||
            <ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
        </ion-button>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
</ng-template>
 | 
			
		||||
							
								
								
									
										131
									
								
								src/core/features/login/components/sites/sites.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								src/core/features/login/components/sites/sites.ts
									
									
									
									
									
										Normal file
									
								
							@ -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<void> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        await ModalController.dismiss(closeAll);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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. <strong>Please use the URL of your school or organization's site.</strong>",
 | 
			
		||||
    "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",
 | 
			
		||||
 | 
			
		||||
@ -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 {}
 | 
			
		||||
 | 
			
		||||
@ -35,7 +35,9 @@ const appRoutes: Routes = [
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [AppRoutingModule.forChild(appRoutes)],
 | 
			
		||||
    imports: [
 | 
			
		||||
        AppRoutingModule.forChild(appRoutes),
 | 
			
		||||
    ],
 | 
			
		||||
    providers: [
 | 
			
		||||
        {
 | 
			
		||||
            provide: APP_INITIALIZER,
 | 
			
		||||
 | 
			
		||||
@ -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) {
 | 
			
		||||
 | 
			
		||||
@ -4,11 +4,11 @@
 | 
			
		||||
            <ion-back-button [text]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
 | 
			
		||||
        <h1>{{ 'core.settings.sites' | translate }}</h1>
 | 
			
		||||
        <h1>{{ 'core.login.accounts' | translate }}</h1>
 | 
			
		||||
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <ion-button fill="clear" *ngIf="sites && sites.length > 0" (click)="toggleDelete()"
 | 
			
		||||
                [attr.aria-label]="'core.delete' | translate">
 | 
			
		||||
            <ion-button fill="clear" *ngIf="accountsList.count > 0" (click)="toggleDelete()"
 | 
			
		||||
                [attr.aria-label]="'core.login.toggleremove' | translate">
 | 
			
		||||
                <ion-icon slot="icon-only" name="fas-pen" aria-hidden="true"></ion-icon>
 | 
			
		||||
            </ion-button>
 | 
			
		||||
            <ion-button (click)="openSettings()" [attr.aria-label]="'core.settings.appsettings' | translate">
 | 
			
		||||
@ -18,31 +18,43 @@
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ion-list>
 | 
			
		||||
        <ion-item button (click)="login(site.id)" *ngFor="let site of sites" detail="true">
 | 
			
		||||
            <ion-avatar slot="start">
 | 
			
		||||
                <img [src]="site.avatar" core-external-content [siteId]="site.id"
 | 
			
		||||
                    alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}" onError="this.src='assets/img/user-avatar.png'">
 | 
			
		||||
            </ion-avatar>
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <p class="item-heading">{{site.fullName}}</p>
 | 
			
		||||
                <p><core-format-text [text]="site.siteName" clean="true" [siteId]="site.id"></core-format-text></p>
 | 
			
		||||
                <p>{{site.siteUrl}}</p>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
            <ion-badge slot="end" *ngIf="!showDelete && site.badge">
 | 
			
		||||
                <span aria-hidden="true">{{site.badge}}</span>
 | 
			
		||||
                <span class="sr-only">{{ 'core.login.sitebadgedescription' | translate:{ count: site.badge } }}</span>
 | 
			
		||||
            </ion-badge>
 | 
			
		||||
            <ion-button *ngIf="showDelete" slot="end" fill="clear" color="danger" (click)="deleteSite($event, site)"
 | 
			
		||||
                [attr.aria-label]="'core.delete' | translate" [@coreSlideInOut]="'fromRight'">
 | 
			
		||||
                <ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
    </ion-list>
 | 
			
		||||
    <core-loading [hideUntil]="loaded">
 | 
			
		||||
        <ion-list>
 | 
			
		||||
            <ng-container *ngFor="let sites of accountsList.otherSites">
 | 
			
		||||
                <ion-item-divider sticky="true" *ngIf="sites[0]">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>
 | 
			
		||||
                            <core-format-text [text]="sites[0].siteName" clean="true" [siteId]="sites[0].id"></core-format-text>
 | 
			
		||||
                        </h2>
 | 
			
		||||
                        <p><a [href]="sites[0].siteUrl" core-link autoLogin="no">{{ sites[0].siteUrl }}</a></p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item-divider>
 | 
			
		||||
 | 
			
		||||
                <ion-item button *ngFor="let site of sites" (click)="login($event, site.id)" detail="true">
 | 
			
		||||
                    <ion-avatar slot="start">
 | 
			
		||||
                        <img [src]="site.avatar" core-external-content [siteId]="site.id"
 | 
			
		||||
                            alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}" onError="this.src='assets/img/user-avatar.png'">
 | 
			
		||||
                    </ion-avatar>
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <p class="item-heading">{{site.fullName}}</p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-badge slot="end" *ngIf="!showDelete && site.badge" @coreShowHideAnimation>
 | 
			
		||||
                        <span aria-hidden="true">{{site.badge}}</span>
 | 
			
		||||
                        <span class="sr-only">{{ 'core.login.sitebadgedescription' | translate:{ count: site.badge }
 | 
			
		||||
                            }}</span>
 | 
			
		||||
                    </ion-badge>
 | 
			
		||||
                    <ion-button *ngIf="showDelete" slot="end" fill="clear" color="danger" (click)="deleteSite($event, site)"
 | 
			
		||||
                        [attr.aria-label]="'core.login.removeaccount' | translate" [@coreSlideInOut]="'fromRight'">
 | 
			
		||||
                        <ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
                    </ion-button>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
        </ion-list>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
    <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end">
 | 
			
		||||
        <ion-fab-button (click)="add()" [attr.aria-label]="'core.add' | translate">
 | 
			
		||||
        <ion-fab-button (click)="add()" [attr.aria-label]="'core.login.add' | translate">
 | 
			
		||||
            <ion-icon name="fas-plus" aria-hidden="true"></ion-icon>
 | 
			
		||||
            <span class="sr-only">{{ 'core.add' | translate }}</span>
 | 
			
		||||
            <span class="sr-only">{{ 'core.login.add' | translate }}</span>
 | 
			
		||||
        </ion-fab-button>
 | 
			
		||||
    </ion-fab>
 | 
			
		||||
</ion-content>
 | 
			
		||||
 | 
			
		||||
@ -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<void> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
    async deleteSite(event: Event, site: CoreSiteBasicInfo): Promise<void> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
    async login(event: Event, siteId: string): Promise<void> {
 | 
			
		||||
        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();
 | 
			
		||||
 | 
			
		||||
@ -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<void> {
 | 
			
		||||
        const [path, params] = this.getAddSiteRouteInfo(showKeyboard);
 | 
			
		||||
    async goToAddSite(setRoot = false, showKeyboard = false): Promise<void> {
 | 
			
		||||
        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,121 @@ 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<CoreAccountsList> {
 | 
			
		||||
        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.siteUrl.replace(/^https?:\/\//, '').toLowerCase();
 | 
			
		||||
            accountsList.currentSite.siteUrl = siteUrl;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const otherSites: Record<string, CoreSiteBasicInfo[]> = {};
 | 
			
		||||
 | 
			
		||||
        // Add site counter and classify sites.
 | 
			
		||||
        await Promise.all(sites.map(async (site) => {
 | 
			
		||||
            site.siteUrl = site.siteUrl.replace(/^https?:\/\//, '').toLowerCase();
 | 
			
		||||
            site.badge = await CoreUtils.ignoreErrors(CorePushNotifications.getSiteCounter(site.id)) || 0;
 | 
			
		||||
 | 
			
		||||
            if (site.siteUrl == siteUrl) {
 | 
			
		||||
                accountsList.sameSite.push(site);
 | 
			
		||||
            } else {
 | 
			
		||||
                if (!otherSites[site.siteUrl]) {
 | 
			
		||||
                    otherSites[site.siteUrl] = [];
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                otherSites[site.siteUrl].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<void> {
 | 
			
		||||
        await CoreSites.deleteSite(site.id);
 | 
			
		||||
 | 
			
		||||
        const siteUrl = site.siteUrl;
 | 
			
		||||
        let index = 0;
 | 
			
		||||
 | 
			
		||||
        // Found on same site.
 | 
			
		||||
        if (accountsList.sameSite.length > 0 && accountsList.sameSite[0].siteUrl == 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].siteUrl == 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.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ 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: [
 | 
			
		||||
@ -24,6 +25,7 @@ import { CoreMainMenuUserMenuComponent } from './user-menu/user-menu';
 | 
			
		||||
    ],
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
        CoreLoginComponentsModule,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [
 | 
			
		||||
        CoreMainMenuUserButtonComponent,
 | 
			
		||||
 | 
			
		||||
@ -8,59 +8,71 @@
 | 
			
		||||
        <h1>
 | 
			
		||||
            {{'core.user.account' | translate}}
 | 
			
		||||
        </h1>
 | 
			
		||||
        <ion-buttons slot="end" *ngIf="loaded">
 | 
			
		||||
            <ion-button fill="clear" *ngIf="moreSites" (click)="switchAccounts($event)"
 | 
			
		||||
                [attr.aria-label]="'core.mainmenu.changeaccount' | translate">
 | 
			
		||||
                <ion-icon name="fas-exchange-alt" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
            </ion-button>
 | 
			
		||||
            <ion-button fill="clear" *ngIf="!moreSites" (click)="addAccount($event)"
 | 
			
		||||
                [attr.aria-label]="'core.login.add' | translate">
 | 
			
		||||
                <ion-icon name="fas-user-plus" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <core-loading [hideUntil]="true">
 | 
			
		||||
        <ion-list>
 | 
			
		||||
            <ion-item class="ion-text-center core-user-profile-maininfo" *ngIf="siteInfo" lines="none">
 | 
			
		||||
                <core-user-avatar [user]="siteInfo" [userId]="siteInfo.userid" [linkProfile]="false"></core-user-avatar>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <h2>{{ siteInfo.fullname }}</h2>
 | 
			
		||||
                    <p class="core-usermenu-siteinfo core-usermenu-sitename">
 | 
			
		||||
                        <core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0" [wsNotFiltered]="true">
 | 
			
		||||
                        </core-format-text>
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p class="core-usermenu-siteinfo core-usermenu-siteurl">
 | 
			
		||||
                        <a [href]="siteUrl" core-link autoLogin="yes">{{ siteUrl }}</a>
 | 
			
		||||
                    </p>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
    <ion-list>
 | 
			
		||||
        <ion-item class="ion-text-center core-user-profile-maininfo" *ngIf="siteInfo" lines="none">
 | 
			
		||||
            <core-user-avatar [user]="siteInfo" [userId]="siteInfo.userid" [linkProfile]="false"></core-user-avatar>
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <h2>{{ siteInfo.fullname }}</h2>
 | 
			
		||||
                <p class="core-usermenu-siteinfo core-usermenu-sitename">
 | 
			
		||||
                    <core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0"
 | 
			
		||||
                        [wsNotFiltered]="true">
 | 
			
		||||
                    </core-format-text>
 | 
			
		||||
                </p>
 | 
			
		||||
                <p class="core-usermenu-siteinfo core-usermenu-siteurl">
 | 
			
		||||
                    <a [href]="siteUrl" core-link autoLogin="yes">{{ siteUrl }}</a>
 | 
			
		||||
                </p>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
            <ion-item button class="ion-text-wrap core-usermenu-handler" (click)="openUserProfile($event)"
 | 
			
		||||
                [attr.aria-label]="'core.user.details' | translate" detail="true" lines="none">
 | 
			
		||||
                <ion-icon name="fas-user" slot="start" aria-hidden="true"></ion-icon>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <p class="item-heading">{{ 'core.user.details' | translate }}</p>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        <ion-item button class="ion-text-wrap core-usermenu-handler" (click)="openUserProfile($event)"
 | 
			
		||||
            [attr.aria-label]="'core.user.details' | translate" detail="true" lines="none">
 | 
			
		||||
            <ion-icon name="fas-user" slot="start" aria-hidden="true"></ion-icon>
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <p class="item-heading">{{ 'core.user.details' | translate }}</p>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
            <ion-item class="ion-text-center" *ngIf="(!handlers || !handlers.length) && !handlersLoaded" lines="none">
 | 
			
		||||
                <ion-label><ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner></ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        <ion-item class="ion-text-center" *ngIf="(!handlers || !handlers.length) && !handlersLoaded" lines="none">
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
            <ion-item button *ngFor="let handler of handlers" class="ion-text-wrap" (click)="handlerClicked($event, handler)"
 | 
			
		||||
                [ngClass]="['core-user-menu-handler', handler.class || '']" [hidden]="handler.hidden"
 | 
			
		||||
                [attr.aria-label]="handler.title | translate" detail="true" lines="none">
 | 
			
		||||
                <ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <p class="item-heading">{{ handler.title | translate }}</p>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        <ion-item button *ngFor="let handler of handlers" class="ion-text-wrap"
 | 
			
		||||
            (click)="handlerClicked($event, handler)" [ngClass]="['core-user-menu-handler', handler.class || '']"
 | 
			
		||||
            [hidden]="handler.hidden" [attr.aria-label]="handler.title | translate" detail="true" lines="none">
 | 
			
		||||
            <ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon>
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <p class="item-heading">{{ handler.title | translate }}</p>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
            <ion-item button (click)="openPreferences($event)" [attr.aria-label]="'core.settings.preferences' | translate" detail="true"
 | 
			
		||||
                class="core-user-menu-preferences">
 | 
			
		||||
                <ion-icon name="fas-wrench" slot="start" aria-hidden="true"></ion-icon>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <p class="item-heading">{{ 'core.settings.preferences' | translate }}</p>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-list>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
        <ion-item button (click)="openPreferences($event)" [attr.aria-label]="'core.settings.preferences' | translate"
 | 
			
		||||
            detail="true" class="core-user-menu-preferences">
 | 
			
		||||
            <ion-icon name="fas-wrench" slot="start" aria-hidden="true"></ion-icon>
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <p class="item-heading">{{ 'core.settings.preferences' | translate }}</p>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
    </ion-list>
 | 
			
		||||
</ion-content>
 | 
			
		||||
<ion-footer class="ion-padding">
 | 
			
		||||
    <ion-button (click)="logout($event)" expand="block" color="danger" [attr.aria-label]="logoutLabel | translate" class="ion-text-wrap">
 | 
			
		||||
    <ion-button (click)="logout($event)" expand="block" color="danger"
 | 
			
		||||
        [attr.aria-label]="'core.mainmenu.logout' | translate" class="ion-text-wrap">
 | 
			
		||||
        <ion-icon name="fas-sign-out-alt" slot="start" aria-hidden="true"></ion-icon>
 | 
			
		||||
        {{ logoutLabel | translate }}
 | 
			
		||||
        {{ 'core.mainmenu.logout' | translate }}
 | 
			
		||||
    </ion-button>
 | 
			
		||||
</ion-footer>
 | 
			
		||||
</ion-footer>
 | 
			
		||||
 | 
			
		||||
@ -14,11 +14,13 @@
 | 
			
		||||
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
@ -34,11 +36,12 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    siteInfo?: CoreSiteInfo;
 | 
			
		||||
    siteName?: string;
 | 
			
		||||
    logoutLabel = 'core.mainmenu.changesite';
 | 
			
		||||
    siteUrl?: string;
 | 
			
		||||
    handlers: CoreUserProfileHandlerData[] = [];
 | 
			
		||||
    handlersLoaded = false;
 | 
			
		||||
    loaded = false;
 | 
			
		||||
    user?: CoreUserProfile;
 | 
			
		||||
    moreSites = false;
 | 
			
		||||
 | 
			
		||||
    protected subscription!: Subscription;
 | 
			
		||||
 | 
			
		||||
@ -46,13 +49,16 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        // 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.logoutLabel = CoreLoginHelper.getLogoutLabel(currentSite);
 | 
			
		||||
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
 | 
			
		||||
        // Load the handlers.
 | 
			
		||||
        if (this.siteInfo) {
 | 
			
		||||
@ -133,6 +139,38 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
 | 
			
		||||
        CoreSites.logout();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show account selector.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Click event
 | 
			
		||||
     */
 | 
			
		||||
    async switchAccounts(event: Event): Promise<void> {
 | 
			
		||||
        const thisModal = await ModalController.getTop();
 | 
			
		||||
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        const closeAll = await CoreDomUtils.openSideModal<boolean>({
 | 
			
		||||
            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<void> {
 | 
			
		||||
        await this.close(event);
 | 
			
		||||
 | 
			
		||||
        await CoreLoginHelper.goToAddSite(true, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Close modal.
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
    "changesite": "Change site",
 | 
			
		||||
    "home": "Home",
 | 
			
		||||
    "logout": "Log out"
 | 
			
		||||
    "logout": "Log out",
 | 
			
		||||
    "switchaccount": "Switch account"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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, {
 | 
			
		||||
 | 
			
		||||
@ -15,12 +15,12 @@
 | 
			
		||||
    "interests": "Interests",
 | 
			
		||||
    "lastname": "Surname",
 | 
			
		||||
    "manager": "Manager",
 | 
			
		||||
    "myprofile": "My profile",
 | 
			
		||||
    "newpicture": "New picture",
 | 
			
		||||
    "noparticipants": "No participants found for this course",
 | 
			
		||||
    "participants": "Participants",
 | 
			
		||||
    "phone1": "Phone",
 | 
			
		||||
    "phone2": "Mobile phone",
 | 
			
		||||
    "profile": "Profile",
 | 
			
		||||
    "roles": "Roles",
 | 
			
		||||
    "sendemail": "Email",
 | 
			
		||||
    "student": "Student",
 | 
			
		||||
 | 
			
		||||
@ -606,7 +606,7 @@ export class CoreAppProvider {
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            localStorage.setItem('CoreRedirect', JSON.stringify(redirect));
 | 
			
		||||
        } catch (ex) {
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1095,7 +1095,7 @@ export class CoreSitesProvider {
 | 
			
		||||
    async getSortedSites(ids?: string[]): Promise<CoreSiteBasicInfo[]> {
 | 
			
		||||
        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();
 | 
			
		||||
@ -1754,40 +1754,13 @@ 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.
 | 
			
		||||
    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.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user