Merge pull request #2746 from NoelDeMartin/MOBILE-3320

MOBILE-3320: Tweaks
main
Dani Palou 2021-05-06 13:42:36 +02:00 committed by GitHub
commit 7dd67a99b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 357 additions and 120 deletions

4
.gitignore vendored
View File

@ -30,8 +30,8 @@ npm-debug.log*
/www
/src/assets/lib
/moodle.*.config.json
!/moodle.example.config.json
/moodle.config.*.json
!/moodle.config.example.json
/src/assets/lang/*
/src/assets/env.json

View File

@ -37,6 +37,41 @@
],
"description": "[Moodle] Create a Page class"
},
"[Moodle] Module class": {
"scope": "typescript",
"prefix": "mamodule",
"body": [
"import { NgModule } from '@angular/core';",
"",
"@NgModule({",
" $0",
"})",
"export class ${1}Module {}",
""
],
"description": "[Moodle] Create a Module class"
},
"[Moodle] Lazy Module class": {
"scope": "typescript",
"prefix": "malazymodule",
"body": [
"import { NgModule } from '@angular/core';",
"import { RouterModule, Routes } from '@angular/router';",
"",
"const routes: Routes = [",
" $0",
"];",
"",
"@NgModule({",
" imports: [",
" RouterModule.forChild(routes),",
" ],",
"})",
"export class ${1}LazyModule {}",
""
],
"description": "[Moodle] Create a Lazy Module class"
},
"[Moodle] Service Singleton": {
"scope": "typescript",
"prefix": "massingleton",
@ -74,5 +109,5 @@
""
],
"description": "[Moodle] Create a Pure Singleton"
}
},
}

View File

@ -2,7 +2,7 @@
"files.associations": {
"moodle.config.json": "jsonc",
"moodle.*.config.json": "jsonc",
"moodle.config.*.json": "jsonc",
},
}

View File

@ -25,7 +25,7 @@ function getConfig(environment) {
};
const config = parseJsonc(readFileSync(resolve(__dirname, '../moodle.config.json')).toString());
const envSuffixes = (envSuffixesMap[environment] || []);
const envConfigPath = envSuffixes.map(suffix => resolve(__dirname, `../moodle.${suffix}.config.json`)).find(existsSync);
const envConfigPath = envSuffixes.map(suffix => resolve(__dirname, `../moodle.config.${suffix}.json`)).find(existsSync);
if (envConfigPath) {
const envConfig = parseJsonc(readFileSync(envConfigPath).toString());

View File

@ -48,5 +48,5 @@ gulp.task('default', gulp.parallel(['lang', 'env']));
gulp.task('watch', () => {
gulp.watch(paths.lang, { interval: 500 }, gulp.parallel('lang'));
gulp.watch(['./moodle.config.json', './moodle.*.config.json'], { interval: 500 }, gulp.parallel('env'));
gulp.watch(['./moodle.config.json', './moodle.config.*.json'], { interval: 500 }, gulp.parallel('env'));
});

View File

@ -380,7 +380,9 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
}
// Fill user data for Offline discussions (should be already cached).
const promises = offlineDiscussions.map(async (discussion: any) => {
const promises = offlineDiscussions.map(async (offlineDiscussion) => {
const discussion = offlineDiscussion as unknown as AddonModForumDiscussion;
if (discussion.parent === 0 || forum.type === 'single') {
// Do not show author for first post and type single.
return;

View File

@ -71,8 +71,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
@Input() discussion?: AddonModForumDiscussion; // Post's' discussion, only for starting posts.
@Input() component!: string; // Component this post belong to.
@Input() componentId!: number; // Component ID.
@Input() replyData: any; // Object with the new post data. Usually shared between posts.
@Input() originalData: any; // Object with the original post data. Usually shared between posts.
@Input() replyData!: AddonModForumReply; // Object with the new post data. Usually shared between posts.
@Input() originalData!: Omit<AddonModForumReply, 'id'>; // Object with the original post data. Usually shared between posts.
@Input() trackPosts!: boolean; // True if post is being tracked.
@Input() forum!: AddonModForumData; // The forum the post belongs to. Required for attachments and offline posts.
@Input() accessInfo!: AddonModForumAccessInformation; // Forum access information.
@ -103,7 +103,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
get showForm(): boolean {
return this.post.id > 0
? !this.replyData.isEditing && this.replyData.replyingTo === this.post.id
: this.replyData.isEditing && this.replyData.replyingTo === this.post.parentid;
: !!this.replyData.isEditing && this.replyData.replyingTo === this.post.parentid;
}
/**
@ -275,7 +275,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
}
// Add some HTML to the message if needed.
const message = CoreTextUtils.formatHtmlLines(data.message);
const message = CoreTextUtils.formatHtmlLines(data.message!);
const files = data.files;
const options: AddonModForumUpdateDiscussionPostWSOptionsObject = {};
@ -295,14 +295,14 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
}
// Try to send it to server.
const sent = await AddonModForum.updatePost(this.post.id, data.subject, message, options);
const sent = await AddonModForum.updatePost(this.post.id, data.subject!, message, options);
if (sent && this.forum.id) {
// Data sent to server, delete stored files (if any).
AddonModForumHelper.deleteReplyStoredFiles(this.forum.id, this.post.id);
this.onPostChange.emit();
this.post.subject = data.subject;
this.post.subject = data.subject!;
this.post.message = message;
this.post.attachments = data.files;
}
@ -419,7 +419,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
let saveOffline = false;
let message = this.replyData.message;
const subject = this.replyData.subject;
const replyingTo = this.replyData.replyingTo;
const replyingTo = this.replyData.replyingTo!;
const files = this.replyData.files || [];
const options: AddonModForumReplyOptions = {};
const modal = await CoreDomUtils.showModalLoading('core.sending', true);

View File

@ -38,6 +38,7 @@ import {
AddonModForumDiscussion,
AddonModForumPost,
AddonModForumProvider,
AddonModForumReply,
} from '../../services/forum';
import { AddonModForumHelper } from '../../services/forum-helper';
import { AddonModForumOffline } from '../../services/forum-offline';
@ -72,18 +73,18 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
postHasOffline!: boolean;
sort: SortType = 'nested';
trackPosts!: boolean;
replyData = {
replyData: Omit<AddonModForumReply, 'id'> = {
replyingTo: 0,
isEditing: false,
subject: '',
message: null, // Null means empty or just white space.
message: null,
files: [],
isprivatereply: false,
};
originalData = {
subject: null, // Null means original data is not set.
message: null, // Null means empty or just white space.
originalData: Omit<AddonModForumReply, 'id'> = {
subject: null,
message: null,
files: [],
isprivatereply: false,
};

View File

@ -368,25 +368,25 @@ export class AddonModForumHelperProvider {
/**
* Check if the data of a post/discussion has changed.
*
* @param post Current data.
* @param reply Current data.
* @param original Original ata.
* @return True if data has changed, false otherwise.
*/
hasPostDataChanged(post: any, original?: any): boolean {
hasPostDataChanged(reply: AddonModForumPostData, original?: AddonModForumPostData): boolean {
if (!original || original.subject == null) {
// There is no original data, assume it hasn't changed.
return false;
}
if (post.subject != original.subject || post.message != original.message) {
if (reply.subject != original.subject || reply.message != original.message) {
return true;
}
if (post.isprivatereply != original.isprivatereply) {
if (reply.isprivatereply != original.isprivatereply) {
return true;
}
return CoreFileUploader.areFileListDifferent(post.files, original.files);
return CoreFileUploader.areFileListDifferent(reply.files ?? [], original.files ?? []);
}
/**
@ -541,3 +541,13 @@ export class AddonModForumHelperProvider {
}
export const AddonModForumHelper = makeSingleton(AddonModForumHelperProvider);
/**
* Forum post data used to check changes.
*/
type AddonModForumPostData = {
subject?: string | null;
message?: string | null;
isprivatereply?: boolean;
files?: CoreFileEntry[];
};

View File

@ -1407,7 +1407,7 @@ export type AddonModForumDiscussion = {
mailnow: number; // Mail now?.
userfullname: string | boolean; // Post author full name.
usermodifiedfullname: string; // Post modifier full name.
userpictureurl: string; // Post author picture.
userpictureurl?: string; // Post author picture.
usermodifiedpictureurl: string; // Post modifier picture.
numreplies: number; // The number of replies in the discussion.
numunread: number; // The number of unread discussions.
@ -1564,9 +1564,12 @@ export type AddonModForumAccessInformation = {
*/
export type AddonModForumReply = {
id: number;
subject: string;
message: string;
subject: string | null; // Null means original data is not set.
message: string | null; // Null means empty or just white space.
files: CoreFileEntry[];
replyingTo?: number;
isEditing?: boolean;
isprivatereply?: boolean;
};
/**

View File

@ -45,7 +45,7 @@ export class AddonModForumDiscussionLinkHandlerService extends CoreContentLinksH
url: string,
params: Params,
courseId?: number,
data?: any,
data?: { instance?: string; cmid?: string; postid?: string },
): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
data = data || {};
@ -56,13 +56,13 @@ export class AddonModForumDiscussionLinkHandlerService extends CoreContentLinksH
action: (siteId): void => {
const discussionId = parseInt(params.d, 10);
const pageParams: Params = {
forumId: data.instance && parseInt(data.instance, 10),
cmId: data.cmid && parseInt(data.cmid, 10),
forumId: data?.instance && parseInt(data.instance, 10),
cmId: data?.cmid && parseInt(data.cmid, 10),
courseId: courseId || parseInt(params.courseid, 10) || parseInt(params.cid, 10),
};
if (data.postid || params.urlHash) {
pageParams.postId = parseInt(data.postid || params.urlHash.replace('p', ''));
if (data?.postid || params.urlHash) {
pageParams.postId = parseInt(data?.postid || params.urlHash.replace('p', ''));
}
if (params.parent) {

View File

@ -64,7 +64,7 @@ export class AddonModResourcePrefetchHandlerService extends CoreCourseResourcePr
dirPath = await CoreFilepool.getPackageDirPathByUrl(CoreSites.getCurrentSiteId(), module.url!);
}
const promises: Promise<any>[] = [];
const promises: Promise<unknown>[] = [];
promises.push(super.downloadOrPrefetch(module, courseId, prefetch, dirPath));

View File

@ -29,7 +29,7 @@ import { makeSingleton } from '@singletons';
import { CoreEvents } from '@singletons/events';
const SEPARATOR_35 = /\/\*\*? *3\.5(\.0)? *styles? *\*\//i; // A comment like "/* 3.5 styles */".
const TMP_SITE_ID = 'tmpsite';
export const TMP_SITE_ID = 'tmpsite';
/**
* Service to handle remote themes. A remote theme is a CSS sheet stored in the site that allows customising the Mobile app.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 B

View File

@ -560,7 +560,7 @@ export class CoreSite {
return CoreUtils.clone(response);
}
const promise = this.getFromCache<T>(method, data, preSets, false).catch(() => {
const promise = this.getFromCache<T>(method, data, preSets, false).catch(async () => {
if (preSets.forceOffline) {
// Don't call the WS, just fail.
throw new CoreError(
@ -569,13 +569,15 @@ export class CoreSite {
}
// Call the WS.
return this.callOrEnqueueRequest<T>(method, data, preSets, wsPreSets).then((response) => {
try {
const response = await this.callOrEnqueueRequest<T>(method, data, preSets, wsPreSets);
if (preSets.saveToCache) {
this.saveToCache(method, data, response, preSets);
}
return response;
}).catch((error) => {
} catch (error) {
if (error.errorcode == 'invalidtoken' ||
(error.errorcode == 'accessexception' && error.message.indexOf('Invalid token - token expired') > -1)) {
if (initialToken !== this.token && !retrying) {
@ -585,7 +587,9 @@ export class CoreSite {
return this.request<T>(method, data, preSets, true);
} else if (CoreApp.isSSOAuthenticationOngoing()) {
// There's an SSO authentication ongoing, wait for it to finish and try again.
return CoreApp.waitForSSOAuthentication().then(() => this.request<T>(method, data, preSets, true));
await CoreApp.waitForSSOAuthentication();
return this.request<T>(method, data, preSets, true);
}
// Session expired, trigger event.
@ -649,9 +653,7 @@ export class CoreSite {
if (preSets.deleteCacheIfWSError && CoreUtils.isWebServiceError(error)) {
// Delete the cache entry and return the entry. Don't block the user with the delete.
this.deleteFromCache(method, data, preSets).catch(() => {
// Ignore errors.
});
CoreUtils.ignoreErrors(this.deleteFromCache(method, data, preSets));
throw new CoreWSError(error);
}
@ -660,10 +662,12 @@ export class CoreSite {
preSets.omitExpires = true;
preSets.getFromCache = true;
return this.getFromCache<T>(method, data, preSets, true).catch(() => {
try {
return await this.getFromCache<T>(method, data, preSets, true);
} catch (e) {
throw new CoreWSError(error);
});
});
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}).then((response: any) => {
// Check if the response is an error, this happens if the error was stored in the cache.

View File

@ -38,10 +38,18 @@ export class CoreConstants {
/* eslint-disable max-len */
static readonly SECONDS_YEAR = 31536000;
static readonly SECONDS_MONTH = 2592000;
static readonly SECONDS_WEEK = 604800;
static readonly SECONDS_DAY = 86400;
static readonly SECONDS_HOUR = 3600;
static readonly SECONDS_MINUTE = 60;
static readonly MILLISECONDS_YEAR = 31536000000;
static readonly MILLISECONDS_MONTH = 2592000000;
static readonly MILLISECONDS_WEEK = 604800000;
static readonly MILLISECONDS_DAY = 86400000;
static readonly MILLISECONDS_HOUR = 3600000;
static readonly MILLISECONDS_MINUTE = 60000;
static readonly MILLISECONDS_SECOND = 1000;
static readonly WIFI_DOWNLOAD_THRESHOLD = 104857600; // 100MB.
static readonly DOWNLOAD_THRESHOLD = 10485760; // 10MB.
static readonly MINIMUM_FREE_SPACE = 10485760; // 10MB.
@ -131,7 +139,7 @@ export class CoreConstants {
}
type EnvironmentConfig = {
export interface EnvironmentConfig {
app_id: string;
appname: string;
versioncode: number;
@ -167,7 +175,7 @@ type EnvironmentConfig = {
forceOpenLinksIn: 'app' | 'browser';
};
type EnvironmentBuild = {
export interface EnvironmentBuild {
version: string;
isProduction: boolean;
isTesting: boolean;

View File

@ -31,7 +31,7 @@ import { Directive, ElementRef, OnInit, Input, Output, EventEmitter } from '@ang
*
* Example usage:
*
* <a ion-button [core-suppress-events] (onClick)="toggle($event)">
* <ion-button [core-suppress-events] (onClick)="toggle($event)">
*/
@Directive({
selector: '[core-suppress-events]',

View File

@ -34,10 +34,10 @@ export class CoreBlockComponent implements OnInit, OnDestroy, DoCheck {
@Input() block!: CoreCourseBlock; // The block to render.
@Input() contextLevel!: string; // The context where the block will be used.
@Input() instanceId!: number; // The instance ID associated with the context level.
@Input() extraData: any; // Any extra data to be passed to the block.
@Input() extraData!: Record<string, unknown>; // Any extra data to be passed to the block.
componentClass?: Type<unknown>; // The class of the component to render.
data: any = {}; // Data to pass to the component.
data: Record<string, unknown> = {}; // Data to pass to the component.
class?: string; // CSS class to apply to the block.
loaded = false;

View File

@ -140,17 +140,17 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy {
*
* @param e Click event.
*/
prefetchCourse(e: Event): void {
e.preventDefault();
e.stopPropagation();
async prefetchCourse(e?: Event): Promise<void> {
e?.preventDefault();
e?.stopPropagation();
/* @ todo try {
CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course);
try {
await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course);
} catch (error) {
if (!this.isDestroyed) {
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
}
}*/
}
}
/**

View File

@ -112,7 +112,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
this.siteConfig = config;
await CoreSites.checkRequiredMinimumVersion(config);
await CoreSites.checkApplication(config);
// Check logoURL if user avatar is not set.
if (this.userAvatar.startsWith(this.siteUrl + '/theme/image.php')) {

View File

@ -63,7 +63,7 @@
<ion-spinner></ion-spinner>
</div>
<ng-container *ngFor="let site of sites">
<ng-container *ngTemplateOutlet="site; context: {site: site}"></ng-container>
<ng-container *ngTemplateOutlet="sitelisting; context: {site: site}"></ng-container>
</ng-container>
</div>
</ion-list>
@ -118,7 +118,7 @@
</ion-content>
<!-- Template site selector. -->
<ng-template #site let-site="site">
<ng-template #sitelisting let-site="site">
<ion-item button (click)="connect($event, site.url, site)" [attr.aria-label]="site.name" detail="true">
<ion-thumbnail *ngIf="siteFinderSettings.displayimage" slot="start">
<img [src]="site.imageurl" *ngIf="site.imageurl" onError="this.src='assets/icon/icon.png'" alt="" role="presentation">

View File

@ -348,32 +348,36 @@ export class CoreLoginSitePage implements OnInit {
* @return Promise resolved after logging in.
*/
protected async login(response: CoreSiteCheckResponse, foundSite?: CoreLoginSiteInfoExtended): Promise<void> {
await CoreUtils.ignoreErrors(CoreSites.checkApplication(response));
try {
await CoreSites.checkApplication(response.config);
CoreForms.triggerFormSubmittedEvent(this.formElement, true);
CoreForms.triggerFormSubmittedEvent(this.formElement, true);
if (response.warning) {
CoreDomUtils.showErrorModal(response.warning, true, 4000);
}
if (CoreLoginHelper.isSSOLoginNeeded(response.code)) {
// SSO. User needs to authenticate in a browser.
CoreLoginHelper.confirmAndOpenBrowserForSSOLogin(
response.siteUrl,
response.code,
response.service,
response.config?.launchurl,
);
} else {
const pageParams = { siteUrl: response.siteUrl, siteConfig: response.config };
if (foundSite && !this.fixedSites) {
pageParams['siteName'] = foundSite.name;
pageParams['logoUrl'] = foundSite.imageurl;
if (response.warning) {
CoreDomUtils.showErrorModal(response.warning, true, 4000);
}
CoreNavigator.navigate('/login/credentials', {
params: pageParams,
});
if (CoreLoginHelper.isSSOLoginNeeded(response.code)) {
// SSO. User needs to authenticate in a browser.
CoreLoginHelper.confirmAndOpenBrowserForSSOLogin(
response.siteUrl,
response.code,
response.service,
response.config?.launchurl,
);
} else {
const pageParams = { siteUrl: response.siteUrl, siteConfig: response.config };
if (foundSite && !this.fixedSites) {
pageParams['siteName'] = foundSite.name;
pageParams['logoUrl'] = foundSite.imageurl;
}
CoreNavigator.navigate('/login/credentials', {
params: pageParams,
});
}
} catch (error) {
// Ignore errors.
}
}
@ -541,7 +545,7 @@ export class CoreLoginSitePage implements OnInit {
// Check if site uses SSO.
const response = await CoreSites.checkSite(siteUrl);
await CoreSites.checkApplication(response);
await CoreSites.checkApplication(response.config);
if (!CoreLoginHelper.isSSOLoginNeeded(response.code)) {
// No SSO, go to credentials page.

View File

@ -13,7 +13,6 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreSitePublicConfigResponse } from '@classes/site';
import { CoreCronHandler } from '@services/cron';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
@ -40,9 +39,9 @@ export class CoreLoginCronHandlerService implements CoreCronHandler {
// Do not check twice in the same 10 minutes.
const site = await CoreSites.getSite(siteId);
const config = await CoreUtils.ignoreErrors(site.getPublicConfig(), <Partial<CoreSitePublicConfigResponse>> {});
const config = await CoreUtils.ignoreErrors(site.getPublicConfig());
CoreUtils.ignoreErrors(CoreSites.checkApplication(<any> config));
CoreUtils.ignoreErrors(CoreSites.checkApplication(config));
}
/**

View File

@ -40,6 +40,7 @@ import {
} from './database/pushnotifications';
import { CoreError } from '@classes/errors/error';
import { CoreWSExternalWarning } from '@services/ws';
import { CoreSitesFactory } from '@services/sites-factory';
/**
* Service to handle push notifications.
@ -751,7 +752,7 @@ export class CorePushNotificationsProvider {
await Promise.all(results.map(async (result) => {
// Create a temporary site to unregister.
const tmpSite = new CoreSite(
const tmpSite = CoreSitesFactory.makeSite(
result.siteid,
result.siteurl,
result.token,

View File

@ -309,6 +309,7 @@ export class CoreNavigatorService {
* @return Value of the parameter, undefined if not found.
*/
getRouteParam<T = unknown>(name: string, routeOptions: CoreNavigatorCurrentRouteOptions = {}): T | undefined {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let value: any;
if (!routeOptions.params) {

View File

@ -0,0 +1,52 @@
// (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 { CoreSite, CoreSiteConfig, CoreSiteInfo } from '@classes/site';
import { makeSingleton } from '@singletons';
/*
* Provider to create sites instances.
*/
@Injectable({ providedIn: 'root' })
export class CoreSitesFactoryService {
/**
* Make a site object.
*
* @param id Site ID.
* @param siteUrl Site URL.
* @param token Site's WS token.
* @param info Site info.
* @param privateToken Private token.
* @param config Site public config.
* @param loggedOut Whether user is logged out.
* @return Site instance.
*/
makeSite(
id: string | undefined,
siteUrl: string,
token?: string,
info?: CoreSiteInfo,
privateToken?: string,
config?: CoreSiteConfig,
loggedOut?: boolean,
): CoreSite {
return new CoreSite(id, siteUrl, token, info, privateToken, config, loggedOut);
}
}
export const CoreSitesFactory = makeSingleton(CoreSitesFactoryService);

View File

@ -51,6 +51,7 @@ import {
import { CoreArray } from '../singletons/array';
import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreNavigationOptions } from './navigator';
import { CoreSitesFactory } from './sites-factory';
export const CORE_SITE_SCHEMAS = new InjectionToken<CoreSiteSchema[]>('CORE_SITE_SCHEMAS');
@ -220,7 +221,7 @@ export class CoreSitesProvider {
}
// Site exists. Create a temporary site to check if local_mobile is installed.
const temporarySite = new CoreSite(undefined, siteUrl);
const temporarySite = CoreSitesFactory.makeSite(undefined, siteUrl);
let data: LocalMobileResponse;
try {
@ -438,7 +439,7 @@ export class CoreSitesProvider {
}
// Create a "candidate" site to fetch the site info.
let candidateSite = new CoreSite(undefined, siteUrl, token, undefined, privateToken, undefined, undefined);
let candidateSite = CoreSitesFactory.makeSite(undefined, siteUrl, token, undefined, privateToken, undefined, undefined);
let isNewSite = true;
try {
@ -706,20 +707,19 @@ export class CoreSitesProvider {
/**
* Check the app for a site and show a download dialogs if necessary.
*
* @param response Data obtained during site check.
* @param config Config object of the site.
*/
async checkApplication(response: CoreSiteCheckResponse): Promise<void> {
await this.checkRequiredMinimumVersion(response.config);
async checkApplication(config?: CoreSitePublicConfigResponse): Promise<void> {
await this.checkRequiredMinimumVersion(config);
}
/**
* Check the required minimum version of the app for a site and shows a download dialog.
*
* @param config Config object of the site.
* @param siteId ID of the site to check. Current site id will be used otherwise.
* @param config Config object of the site.
* @return Resolved with if meets the requirements, rejected otherwise.
*/
async checkRequiredMinimumVersion(config?: CoreSitePublicConfigResponse, siteId?: string): Promise<void> {
protected async checkRequiredMinimumVersion(config?: CoreSitePublicConfigResponse): Promise<void> {
if (!config || !config.tool_mobile_minimumversion) {
return;
}
@ -735,7 +735,7 @@ export class CoreSitesProvider {
default: config.tool_mobile_setuplink,
};
siteId = siteId || this.getCurrentSiteId();
const siteId = this.getCurrentSiteId();
const downloadUrl = CoreApp.getAppStoreUrl(storesConfig);
@ -837,7 +837,7 @@ export class CoreSitesProvider {
}
try {
await this.checkRequiredMinimumVersion(config);
await this.checkApplication(config);
this.login(siteId);
// Update site info. We don't block the UI.
@ -1004,7 +1004,15 @@ export class CoreSitesProvider {
const info = entry.info ? <CoreSiteInfo> CoreTextUtils.parseJSON(entry.info) : undefined;
const config = entry.config ? <CoreSiteConfig> CoreTextUtils.parseJSON(entry.config) : undefined;
const site = new CoreSite(entry.id, entry.siteUrl, entry.token, info, entry.privateToken, config, entry.loggedOut == 1);
const site = CoreSitesFactory.makeSite(
entry.id,
entry.siteUrl,
entry.token,
info,
entry.privateToken,
config,
entry.loggedOut == 1,
);
site.setOAuthId(entry.oauthId || undefined);
return this.migrateSiteSchemas(site).then(() => {
@ -1165,28 +1173,27 @@ export class CoreSitesProvider {
* @return Promise resolved when the user is logged out.
*/
async logout(): Promise<void> {
let siteId: string | undefined;
if (!this.currentSite) {
return;
}
const db = await this.appDB;
const promises: Promise<unknown>[] = [];
const siteConfig = this.currentSite.getStoredConfig();
const siteId = this.currentSite.getId();
if (this.currentSite) {
const db = await this.appDB;
const siteConfig = this.currentSite.getStoredConfig();
siteId = this.currentSite.getId();
this.currentSite = undefined;
this.currentSite = undefined;
if (siteConfig && siteConfig.tool_mobile_forcelogout == '1') {
promises.push(this.setSiteLoggedOut(siteId, true));
}
promises.push(db.deleteRecords(CURRENT_SITE_TABLE_NAME, { id: 1 }));
if (siteConfig && siteConfig.tool_mobile_forcelogout == '1') {
promises.push(this.setSiteLoggedOut(siteId, true));
}
try {
await Promise.all(promises);
} finally {
CoreEvents.trigger(CoreEvents.LOGOUT, {}, siteId);
}
promises.push(db.deleteRecords(CURRENT_SITE_TABLE_NAME, { id: 1 }));
await CoreUtils.ignoreErrors(Promise.all(promises));
CoreEvents.trigger(CoreEvents.LOGOUT, {}, siteId);
}
/**
@ -1293,7 +1300,7 @@ export class CoreSitesProvider {
* @param siteid Site's ID.
* @return A promise resolved when the site is updated.
*/
async updateSiteInfo(siteId: string): Promise<void> {
async updateSiteInfo(siteId?: string): Promise<void> {
const site = await this.getSite(siteId);
try {
@ -1430,7 +1437,7 @@ export class CoreSitesProvider {
* @return Promise resolved with the public config.
*/
getSitePublicConfig(siteUrl: string): Promise<CoreSitePublicConfigResponse> {
const temporarySite = new CoreSite(undefined, siteUrl);
const temporarySite = CoreSitesFactory.makeSite(undefined, siteUrl);
return temporarySite.getPublicConfig();
}

View File

@ -66,7 +66,7 @@ export class CoreCustomURLSchemesProvider {
data.siteUrl = result.siteUrl;
await CoreSites.checkApplication(result);
await CoreSites.checkApplication(result.config);
}
return CoreSites.newSite(

View File

@ -1491,7 +1491,7 @@ export class CoreUtilsProvider {
debounce<T extends unknown[]>(fn: (...args: T) => unknown, delay: number): (...args: T) => void {
let timeoutID: number;
const debounced = (...args: unknown[]): void => {
const debounced = (...args: T): void => {
clearTimeout(timeoutID);
timeoutID = window.setTimeout(() => fn.apply(null, args), delay);

View File

@ -16,6 +16,16 @@ import moment from 'moment';
import { CoreConstants } from '@/core/constants';
import { CoreTime } from './time';
/**
* Method to warn that logs are disabled, called only once.
*/
const warnLogsDisabled = CoreTime.once(() => {
// eslint-disable-next-line no-console
console.warn('Log is disabled in production app');
});
/**
* Log function type.
*/
@ -59,8 +69,7 @@ export class CoreLogger {
// Disable log on production and testing.
if (CoreConstants.BUILD.isProduction || CoreConstants.BUILD.isTesting) {
if (CoreConstants.BUILD.isProduction) {
// eslint-disable-next-line no-console
console.warn('Log is disabled in production app');
warnLogsDisabled();
}
// eslint-disable-next-line @typescript-eslint/no-empty-function

View File

@ -0,0 +1,32 @@
// (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.
/**
* Singleton with helper functions for math operations.
*/
export class CoreMath {
/**
* Clamp a value between a minimum and a maximum.
*
* @param value Original value.
* @param min Minimum value.
* @param max Maximum value.
* @return Clamped value.
*/
static clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
}

View File

@ -21,6 +21,17 @@ export type CoreObjectWithoutEmpty<T> = {
*/
export class CoreObject {
/**
* Check if two objects have the same shape and the same leaf values.
*
* @param a First object.
* @param b Second object.
* @return Whether objects are equal.
*/
static deepEquals(a: unknown, b: unknown): boolean {
return JSON.stringify(a) === JSON.stringify(b);
}
/**
* Check whether the given object is empty.
*

View File

@ -0,0 +1,39 @@
// (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.
/**
* Singleton with helper functions for time operations.
*/
export class CoreTime {
/**
* Wrap a function so that it is called only once.
*
* @param fn Function.
* @return Wrapper that will call the underlying function only once.
*/
static once<T extends unknown[]>(fn: (...args: T) => unknown): (...args: T) => void {
let called = false;
return (...args: T) => {
if (called) {
return;
}
called = true;
fn.apply(null, args);
};
}
}

View File

@ -161,3 +161,12 @@ $screen-breakpoints: (
) !default;
$breakpoint-tablet: map-get($screen-breakpoints, tablet), !default;
/*
* Z-indexes.
*
* https://github.com/ionic-team/ionic-framework/blob/master/core/src/themes/ionic.globals.scss
*/
$z-index-overlay: 1001;
$z-index-overlay-wrapper: 10;

View File

@ -37,6 +37,14 @@
flex-direction: row;
}
.margin-bottom-sm { margin-bottom: 8px; }
.margin-bottom-md { margin-bottom: 12px; }
.font-bold { font-weight: bold; }
.font-italic { font-style: italic; }
.font-lg { font-size: 1.7rem; }
.font-sm { font-size: 1.2rem; }
// Correctly inherit ion-text-wrap onto labels.
ion-item.ion-text-wrap ion-label {
white-space: normal !important;

View File

@ -122,7 +122,7 @@
ion-slide {
--background: var(--core-tab-background);
--color: var(--core-tab-color);
--border-color: var(--core-tab-border-colo);
--border-color: var(--core-tab-border-color);
--color-active: var(--core-tab-color-active);
--border-color-active: var(--core-tab-border-color-active);
}

View File

@ -19,6 +19,7 @@ declare module '@ionic/angular' {
export class NavController {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
navigateForward(url: string | UrlTree | any[], options?: NavigationOptions): Promise<boolean | null>;
}

View File

@ -4,6 +4,7 @@ information provided here is intended especially for developers.
=== 3.9.5 ===
- Several functions inside AddonNotificationsProvider have been modified to accept an "options" parameter instead of having several optional parameters.
- Schemas are now registered using Angular providers with the CORE_SITE_SCHEMAS injection token instead of CoreSitesProvider.registerSiteSchema.
=== 3.9.3 ===