forked from EVOgeek/Vmeda.Online
		
	MOBILE-4459 core: Support demo mode
This commit is contained in:
		
							parent
							
								
									38303aadf5
								
							
						
					
					
						commit
						d27d3ddec3
					
				| @ -296,14 +296,18 @@ export class CoreSite { | ||||
|      * @returns Site name. | ||||
|      */ | ||||
|     async getSiteName(): Promise<string> { | ||||
|         if (this.isDemoModeSite()) { | ||||
|             return CoreConstants.CONFIG.appname; | ||||
|         } | ||||
| 
 | ||||
|         if (this.infos?.sitename) { | ||||
|             return this.infos?.sitename; | ||||
|         } | ||||
| 
 | ||||
|         // Fallback.
 | ||||
|         const isSigleFixedSite = await CoreLoginHelper.isSingleFixedSite(); | ||||
|         const isSingleFixedSite = await CoreLoginHelper.isSingleFixedSite(); | ||||
| 
 | ||||
|         if (isSigleFixedSite) { | ||||
|         if (isSingleFixedSite) { | ||||
|             const sites = await CoreLoginHelper.getAvailableSites(); | ||||
| 
 | ||||
|             return sites[0].name; | ||||
| @ -2459,6 +2463,17 @@ export class CoreSite { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the site is a demo mode site. | ||||
|      * | ||||
|      * @returns Whether the site is a demo mode site. | ||||
|      */ | ||||
|     isDemoModeSite(): boolean { | ||||
|         const demoSiteData = CoreLoginHelper.getDemoModeSiteInfo(); | ||||
| 
 | ||||
|         return this.containsUrl(demoSiteData?.url); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -27,7 +27,7 @@ | ||||
|                     <p> | ||||
|                         <core-format-text [text]="site.siteName" clean="true" [siteId]="site.id"></core-format-text> | ||||
|                     </p> | ||||
|                     <p>{{site.siteUrl}}</p> | ||||
|                     <p *ngIf="!site.isDemoModeSite">{{site.siteUrl}}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|         </ion-list> | ||||
|  | ||||
| @ -28,8 +28,10 @@ | ||||
|                             <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.siteUrlWithoutProtocol }}</a> | ||||
|                         <p *ngIf="!accountsList.currentSite.isDemoModeSite"> | ||||
|                             <a [href]="accountsList.currentSite.siteUrl" core-link autoLogin="yes"> | ||||
|                                 {{ accountsList.currentSite.siteUrlWithoutProtocol }} | ||||
|                             </a> | ||||
|                         </p> | ||||
|                     </ion-label> | ||||
|                 </ion-item-divider> | ||||
| @ -53,7 +55,9 @@ | ||||
|                         <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].siteUrlWithoutProtocol }}</a></p> | ||||
|                         <p *ngIf="!sites[0].isDemoModeSite"> | ||||
|                             <a [href]="sites[0].siteUrl" core-link autoLogin="no">{{ sites[0].siteUrlWithoutProtocol }}</a> | ||||
|                         </p> | ||||
|                     </ion-label> | ||||
|                 </ion-item-divider> | ||||
| 
 | ||||
|  | ||||
| @ -32,7 +32,7 @@ | ||||
|                     <h2 *ngIf="siteName" class="ion-margin-top ion-no-padding core-sitename"> | ||||
|                         <core-format-text [text]="siteName" [filter]="false"></core-format-text> | ||||
|                     </h2> | ||||
|                     <p class="core-siteurl">{{siteUrl}}</p> | ||||
|                     <p class="core-siteurl" *ngIf="!isDemoModeSite">{{siteUrl}}</p> | ||||
|                 </div> | ||||
| 
 | ||||
|                 <core-login-exceeded-attempts *ngIf="exceededAttemptsHTML && supportConfig && loginAttempts >= 3" | ||||
|  | ||||
| @ -61,6 +61,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy { | ||||
|     exceededAttemptsHTML?: SafeHtml | string | null; | ||||
|     siteConfig?: CoreSitePublicConfigResponse; | ||||
|     siteCheckError = ''; | ||||
|     isDemoModeSite = false; | ||||
| 
 | ||||
|     protected siteCheck?: CoreSiteCheckResponse; | ||||
|     protected eventThrown = false; | ||||
| @ -89,8 +90,11 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy { | ||||
|                 this.siteConfig = this.siteCheck.config; | ||||
|             } | ||||
| 
 | ||||
|             this.siteName = CoreNavigator.getRouteParam('siteName'); | ||||
|             this.logoUrl = !CoreConstants.CONFIG.forceLoginLogo && CoreNavigator.getRouteParam('logoUrl') || undefined; | ||||
|             this.isDemoModeSite = CoreLoginHelper.isDemoModeSite(this.siteUrl); | ||||
|             this.siteName = this.isDemoModeSite ? CoreConstants.CONFIG.appname : CoreNavigator.getRouteParam('siteName'); | ||||
|             this.logoUrl = !CoreConstants.CONFIG.forceLoginLogo && !this.isDemoModeSite ? | ||||
|                 CoreNavigator.getRouteParam('logoUrl') : | ||||
|                 undefined; | ||||
|             this.urlToOpen = CoreNavigator.getRouteParam('urlToOpen'); | ||||
|             this.supportConfig = this.siteConfig && new CoreUserGuestSupportConfig(this.siteConfig); | ||||
|         } catch (error) { | ||||
| @ -191,9 +195,13 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (this.isDemoModeSite) { | ||||
|             this.showScanQR = false; | ||||
|         } else { | ||||
|             this.siteName = this.siteConfig.sitename; | ||||
|             this.logoUrl = CoreLoginHelper.getLogoUrl(this.siteConfig); | ||||
|             this.showScanQR = await CoreLoginHelper.displayQRInCredentialsScreen(this.siteConfig.tool_mobile_qrcodetype); | ||||
|         } | ||||
| 
 | ||||
|         const disabledFeatures = CoreLoginHelper.getDisabledFeatures(this.siteConfig); | ||||
|         this.canSignup = this.siteConfig.registerauth == 'email' && | ||||
|  | ||||
| @ -82,7 +82,7 @@ | ||||
|                     <ion-item class="ion-text-wrap ion-text-center"> | ||||
|                         <ion-label> | ||||
|                             <!-- If no sitename show big siteurl. --> | ||||
|                             <p *ngIf="!siteName" class="ion-padding item-heading">{{siteUrl}}</p> | ||||
|                             <p *ngIf="!siteName && !isDemoModeSite" class="ion-padding item-heading">{{siteUrl}}</p> | ||||
|                             <!-- If sitename, show big sitename and small siteurl. --> | ||||
|                             <p *ngIf="siteName" class="ion-padding item-heading"> | ||||
|                                 <core-format-text [text]="siteName" [filter]="false"></core-format-text> | ||||
|  | ||||
| @ -34,6 +34,7 @@ import { CoreForms } from '@singletons/form'; | ||||
| import { CoreRecaptchaComponent } from '@components/recaptcha/recaptcha'; | ||||
| import { CorePath } from '@singletons/path'; | ||||
| import { CoreDom } from '@singletons/dom'; | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| 
 | ||||
| /** | ||||
|  * Page to signup using email. | ||||
| @ -51,6 +52,7 @@ export class CoreLoginEmailSignupPage implements OnInit { | ||||
| 
 | ||||
|     signupForm: FormGroup; | ||||
|     siteUrl!: string; | ||||
|     isDemoModeSite = false; | ||||
|     siteConfig?: CoreSitePublicConfigResponse; | ||||
|     siteName?: string; | ||||
|     authInstructions = ''; | ||||
| @ -127,6 +129,7 @@ export class CoreLoginEmailSignupPage implements OnInit { | ||||
|         } | ||||
| 
 | ||||
|         this.siteUrl = siteUrl; | ||||
|         this.isDemoModeSite = CoreLoginHelper.isDemoModeSite(this.siteUrl); | ||||
| 
 | ||||
|         // Fetch the data.
 | ||||
|         this.fetchData().finally(() => { | ||||
| @ -235,7 +238,7 @@ export class CoreLoginEmailSignupPage implements OnInit { | ||||
|      */ | ||||
|     protected treatSiteConfig(): boolean { | ||||
|         if (this.siteConfig?.registerauth == 'email' && !CoreLoginHelper.isEmailSignupDisabled(this.siteConfig)) { | ||||
|             this.siteName = this.siteConfig.sitename; | ||||
|             this.siteName = this.isDemoModeSite ? CoreConstants.CONFIG.appname : this.siteConfig.sitename; | ||||
|             this.authInstructions = this.siteConfig.authinstructions; | ||||
|             this.ageDigitalConsentVerification = this.siteConfig.agedigitalconsentverification; | ||||
|             this.supportName = this.siteConfig.supportname; | ||||
|  | ||||
| @ -40,7 +40,7 @@ | ||||
|                     <p *ngIf="siteInfo?.siteName" class="ion-no-margin ion-no-padding core-sitename"> | ||||
|                         <core-format-text [text]="siteInfo?.siteName" [filter]="false"></core-format-text> | ||||
|                     </p> | ||||
|                     <p class="core-siteurl">{{siteUrl}}</p> | ||||
|                     <p class="core-siteurl" *ngIf="!isDemoModeSite">{{siteUrl}}</p> | ||||
|                 </div> | ||||
| 
 | ||||
|                 <div class="core-login-user"> | ||||
|  | ||||
| @ -46,6 +46,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     credForm: FormGroup; | ||||
|     siteUrl!: string; | ||||
|     isDemoModeSite = false; | ||||
|     logoUrl?: string; | ||||
|     showForgottenPassword = true; | ||||
|     showUserAvatar = false; | ||||
| @ -101,6 +102,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy { | ||||
|             } | ||||
| 
 | ||||
|             this.siteUrl = site.getURL(); | ||||
|             this.isDemoModeSite = site.isDemoModeSite(); | ||||
| 
 | ||||
|             this.siteInfo = { | ||||
|                 id: this.siteId, | ||||
| @ -112,6 +114,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy { | ||||
|                 siteName: await site.getSiteName(), | ||||
|                 userpictureurl: site.infos.userpictureurl, | ||||
|                 loggedOut: true, // Not used.
 | ||||
|                 isDemoModeSite: this.isDemoModeSite, | ||||
|             }; | ||||
| 
 | ||||
|             this.username = site.infos.username; | ||||
| @ -185,7 +188,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|         await CoreSites.checkApplication(this.siteConfig); | ||||
| 
 | ||||
|         this.logoUrl = CoreLoginHelper.getLogoUrl(this.siteConfig); | ||||
|         this.logoUrl = this.isDemoModeSite ? undefined : CoreLoginHelper.getLogoUrl(this.siteConfig); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -28,7 +28,9 @@ | ||||
|                         <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].siteUrlWithoutProtocol }}</a></p> | ||||
|                         <p *ngIf="!sites[0].isDemoModeSite"> | ||||
|                             <a [href]="sites[0].siteUrl" core-link autoLogin="no">{{ sites[0].siteUrlWithoutProtocol }}</a> | ||||
|                         </p> | ||||
|                     </ion-label> | ||||
|                 </ion-item-divider> | ||||
| 
 | ||||
|  | ||||
| @ -374,14 +374,23 @@ export class CoreLoginHelperProvider { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get Available sites (includes staging sites if are enabled). | ||||
|      * Get Available sites (includes staging sites if are enabled). It doesn't include demo mode site. | ||||
|      * | ||||
|      * @returns Available sites. | ||||
|      */ | ||||
|     async getAvailableSites(): Promise<CoreLoginSiteInfo[]> { | ||||
|         const hasEnabledStagingSites = await CoreSettingsHelper.hasEnabledStagingSites(); | ||||
| 
 | ||||
|         return hasEnabledStagingSites ? CoreConstants.CONFIG.sites : CoreConstants.CONFIG.sites.filter(site => !site.staging); | ||||
|         return CoreConstants.CONFIG.sites.filter(site => (!site.staging || hasEnabledStagingSites) && !site.demoMode); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get demo mode site info. This function doesn't check if demo mode is enabled. | ||||
|      * | ||||
|      * @returns Demo mode site info, undefined if no demo mode site. | ||||
|      */ | ||||
|     getDemoModeSiteInfo(): CoreLoginSiteInfo | undefined { | ||||
|         return CoreConstants.CONFIG.sites.find(site => site.demoMode); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -453,6 +462,14 @@ export class CoreLoginHelperProvider { | ||||
|      * @returns Path and params. | ||||
|      */ | ||||
|     async getAddSiteRouteInfo(showKeyboard?: boolean): Promise<[string, Params]> { | ||||
|         if (CoreConstants.CONFIG.demoMode) { | ||||
|             const demoModeSite = this.getDemoModeSiteInfo(); | ||||
| 
 | ||||
|             if (demoModeSite) { | ||||
|                 return ['/login/credentials', { siteUrl: demoModeSite.url }]; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const sites = await this.getAvailableSites(); | ||||
| 
 | ||||
|         if (sites.length === 1) { | ||||
| @ -583,7 +600,10 @@ export class CoreLoginHelperProvider { | ||||
|         const sites = await this.getAvailableSites(); | ||||
| 
 | ||||
|         if (sites.length) { | ||||
|             return sites.some((site) => CoreUrl.sameDomainAndPath(siteUrl, site.url)); | ||||
|             const demoModeSite = this.getDemoModeSiteInfo(); | ||||
| 
 | ||||
|             return sites.some((site) => CoreUrl.sameDomainAndPath(siteUrl, site.url)) || | ||||
|                 (!!demoModeSite && CoreUrl.sameDomainAndPath(siteUrl, demoModeSite.url)); | ||||
|         } else if (CoreConstants.CONFIG.multisitesdisplay == 'sitefinder' && CoreConstants.CONFIG.onlyallowlistedsites && | ||||
|                 checkSiteFinder) { | ||||
|             // Call the sites finder to validate the site.
 | ||||
| @ -1497,6 +1517,23 @@ export class CoreLoginHelperProvider { | ||||
|         return CoreTextUtils.parseJSON<Record<string, number>>(passwordResetsJson, {}); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a URL belongs to the demo mode site. | ||||
|      * | ||||
|      * @returns Whether the URL belongs to the demo mode site. | ||||
|      */ | ||||
|     isDemoModeSite(url: string): boolean { | ||||
|         const demoSiteData = CoreLoginHelper.getDemoModeSiteInfo(); | ||||
|         if (!demoSiteData) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const demoSiteUrl = CoreTextUtils.addEndingSlash(CoreUrlUtils.removeProtocolAndWWW(demoSiteData.url)); | ||||
|         url = CoreTextUtils.addEndingSlash(CoreUrlUtils.removeProtocolAndWWW(url)); | ||||
| 
 | ||||
|         return demoSiteUrl.indexOf(url) === 0; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const CoreLoginHelper = makeSingleton(CoreLoginHelperProvider); | ||||
|  | ||||
| @ -113,7 +113,7 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy { | ||||
|      * @returns Promise resolved when done. | ||||
|      */ | ||||
|     protected async loadSiteLogo(currentSite: CoreSite): Promise<void> { | ||||
|         if (CoreConstants.CONFIG.forceLoginLogo) { | ||||
|         if (CoreConstants.CONFIG.forceLoginLogo || currentSite.isDemoModeSite()) { | ||||
|             this.siteLogo = 'assets/img/login_logo.png'; | ||||
|             this.siteLogoLoaded = true; | ||||
| 
 | ||||
|  | ||||
| @ -25,8 +25,10 @@ | ||||
|                             <core-format-text [text]="accountsList.currentSite.siteName" clean="true" | ||||
|                                 [siteId]="accountsList.currentSite.id"></core-format-text> | ||||
|                         </p> | ||||
|                         <p><a [href]="accountsList.currentSite.siteUrl" core-link autoLogin="yes">{{ | ||||
|                                 accountsList.currentSite.siteUrlWithoutProtocol }}</a> | ||||
|                         <p *ngIf="!accountsList.currentSite.isDemoModeSite"> | ||||
|                             <a [href]="accountsList.currentSite.siteUrl" core-link autoLogin="yes"> | ||||
|                                 {{ accountsList.currentSite.siteUrlWithoutProtocol }} | ||||
|                             </a> | ||||
|                         </p> | ||||
|                     </ion-label> | ||||
|                 </ion-item-divider> | ||||
| @ -46,7 +48,9 @@ | ||||
|                         <p class="item-heading"> | ||||
|                             <core-format-text [text]="sites[0].siteName" clean="true" [siteId]="sites[0].id"></core-format-text> | ||||
|                         </p> | ||||
|                         <p><a [href]="sites[0].siteUrl" core-link autoLogin="no">{{ sites[0].siteUrlWithoutProtocol }}</a></p> | ||||
|                         <p *ngIf="!sites[0].isDemoModeSite"> | ||||
|                             <a [href]="sites[0].siteUrl" core-link autoLogin="no">{{ sites[0].siteUrlWithoutProtocol }}</a> | ||||
|                         </p> | ||||
|                     </ion-label> | ||||
|                 </ion-item-divider> | ||||
| 
 | ||||
|  | ||||
| @ -58,8 +58,10 @@ | ||||
|                                 <core-format-text [text]="accountsList.currentSite.siteName" clean="true" | ||||
|                                     [siteId]="accountsList.currentSite.id"></core-format-text> | ||||
|                             </p> | ||||
|                             <p><a [href]="accountsList.currentSite.siteUrl" core-link autoLogin="yes">{{ | ||||
|                                     accountsList.currentSite.siteUrlWithoutProtocol }}</a> | ||||
|                             <p *ngIf="!accountsList.currentSite.isDemoModeSite"> | ||||
|                                 <a [href]="accountsList.currentSite.siteUrl" core-link autoLogin="yes"> | ||||
|                                     {{ accountsList.currentSite.siteUrlWithoutProtocol }} | ||||
|                                 </a> | ||||
|                             </p> | ||||
|                         </ion-label> | ||||
|                     </ion-item-divider> | ||||
| @ -79,7 +81,9 @@ | ||||
|                             <p class="item-heading"> | ||||
|                                 <core-format-text [text]="sites[0].siteName" clean="true" [siteId]="sites[0].id"></core-format-text> | ||||
|                             </p> | ||||
|                             <p><a [href]="sites[0].siteUrl" core-link autoLogin="no">{{ sites[0].siteUrlWithoutProtocol }}</a></p> | ||||
|                             <p *ngIf="!sites[0].isDemoModeSite"> | ||||
|                                 <a [href]="sites[0].siteUrl" core-link autoLogin="no">{{ sites[0].siteUrlWithoutProtocol }}</a> | ||||
|                             </p> | ||||
|                         </ion-label> | ||||
|                     </ion-item-divider> | ||||
| 
 | ||||
|  | ||||
| @ -25,7 +25,7 @@ | ||||
|                     <p> | ||||
|                         <core-format-text clean="true" [text]="site.siteName" [siteId]="site.id"></core-format-text> | ||||
|                     </p> | ||||
|                     <p>{{site.siteUrl}}</p> | ||||
|                     <p *ngIf="!site.isDemoModeSite">{{site.siteUrl}}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|         </ion-list> | ||||
|  | ||||
| @ -1303,8 +1303,9 @@ export class CoreSitesProvider { | ||||
|         const formattedSites: CoreSiteBasicInfo[] = []; | ||||
|         sites.forEach((site) => { | ||||
|             if (!ids || ids.indexOf(site.id) > -1) { | ||||
|                 // Parse info.
 | ||||
|                 const isDemoModeSite = CoreLoginHelper.isDemoModeSite(site.siteUrl); | ||||
|                 const siteInfo = site.info ? <CoreSiteInfo> CoreTextUtils.parseJSON(site.info) : undefined; | ||||
| 
 | ||||
|                 const basicInfo: CoreSiteBasicInfo = { | ||||
|                     id: site.id, | ||||
|                     siteUrl: site.siteUrl, | ||||
| @ -1312,10 +1313,11 @@ export class CoreSitesProvider { | ||||
|                     fullname: siteInfo?.fullname, | ||||
|                     firstname: siteInfo?.firstname, | ||||
|                     lastname: siteInfo?.lastname, | ||||
|                     siteName: siteInfo?.sitename, | ||||
|                     siteName: isDemoModeSite ? CoreConstants.CONFIG.appname : siteInfo?.sitename, | ||||
|                     userpictureurl: siteInfo?.userpictureurl, | ||||
|                     siteHomeId: siteInfo?.siteid || 1, | ||||
|                     loggedOut: !!site.loggedOut, | ||||
|                     isDemoModeSite, | ||||
|                 }; | ||||
|                 formattedSites.push(basicInfo); | ||||
|             } | ||||
| @ -2195,6 +2197,7 @@ export type CoreSiteBasicInfo = { | ||||
|     badge?: number; // Badge to display in the site.
 | ||||
|     siteHomeId?: number; // Site home ID.
 | ||||
|     loggedOut: boolean; // If Site is logged out.
 | ||||
|     isDemoModeSite: boolean; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
| @ -2288,6 +2291,11 @@ export type CoreLoginSiteInfo = { | ||||
|      * Class to apply to site item. | ||||
|      */ | ||||
|     className?: string; | ||||
| 
 | ||||
|     /** | ||||
|      * Whether the site is for demo mode usage. | ||||
|      */ | ||||
|     demoMode?: boolean; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  | ||||
							
								
								
									
										1
									
								
								src/types/config.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/types/config.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -73,4 +73,5 @@ export interface EnvironmentConfig { | ||||
|     disableCallWSInBackground?: boolean; // If true, disable calling WS in background.
 | ||||
|     callWSInBackgroundExpirationTime?: number; // Ms to consider an entry expired when calling WS in background. Default: 1 week.
 | ||||
|     disableTokenFile: boolean; // Disable the use of tokenpluginfile.php for downloading files (so it fallbacks to pluginfile.php)
 | ||||
|     demoMode?: boolean; // Whether to run the app in "demo mode".
 | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user