diff --git a/.gitignore b/.gitignore index e120915ae..d3b41f8be 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.vscode/moodle.code-snippets b/.vscode/moodle.code-snippets index 6c7a7923c..6028e1f1d 100644 --- a/.vscode/moodle.code-snippets +++ b/.vscode/moodle.code-snippets @@ -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" - } + }, } diff --git a/.vscode/settings.json b/.vscode/settings.json index d8b699e18..e2f79e54b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "files.associations": { "moodle.config.json": "jsonc", - "moodle.*.config.json": "jsonc", + "moodle.config.*.json": "jsonc", }, } diff --git a/gulp/task-build-env.js b/gulp/task-build-env.js index 1ccb8dec0..b580a230b 100644 --- a/gulp/task-build-env.js +++ b/gulp/task-build-env.js @@ -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()); diff --git a/gulpfile.js b/gulpfile.js index 2115bad2e..605ac7e40 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -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')); }); diff --git a/moodle.example.config.json b/moodle.config.example.json similarity index 100% rename from moodle.example.config.json rename to moodle.config.example.json diff --git a/src/addons/mod/forum/components/index/index.ts b/src/addons/mod/forum/components/index/index.ts index 4562d9917..802085a89 100644 --- a/src/addons/mod/forum/components/index/index.ts +++ b/src/addons/mod/forum/components/index/index.ts @@ -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; diff --git a/src/addons/mod/forum/components/post/post.ts b/src/addons/mod/forum/components/post/post.ts index 7a0405051..7d174bb9b 100644 --- a/src/addons/mod/forum/components/post/post.ts +++ b/src/addons/mod/forum/components/post/post.ts @@ -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; // 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); diff --git a/src/addons/mod/forum/pages/discussion/discussion.page.ts b/src/addons/mod/forum/pages/discussion/discussion.page.ts index edf8d9fdd..b059151df 100644 --- a/src/addons/mod/forum/pages/discussion/discussion.page.ts +++ b/src/addons/mod/forum/pages/discussion/discussion.page.ts @@ -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 = { 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 = { + subject: null, + message: null, files: [], isprivatereply: false, }; diff --git a/src/addons/mod/forum/services/forum-helper.ts b/src/addons/mod/forum/services/forum-helper.ts index 140853e36..0519efe28 100644 --- a/src/addons/mod/forum/services/forum-helper.ts +++ b/src/addons/mod/forum/services/forum-helper.ts @@ -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[]; +}; diff --git a/src/addons/mod/forum/services/forum.ts b/src/addons/mod/forum/services/forum.ts index 582c13e65..c12f4be48 100644 --- a/src/addons/mod/forum/services/forum.ts +++ b/src/addons/mod/forum/services/forum.ts @@ -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; }; /** diff --git a/src/addons/mod/forum/services/handlers/discussion-link.ts b/src/addons/mod/forum/services/handlers/discussion-link.ts index 245d7c558..8e821ed80 100644 --- a/src/addons/mod/forum/services/handlers/discussion-link.ts +++ b/src/addons/mod/forum/services/handlers/discussion-link.ts @@ -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 { 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) { diff --git a/src/addons/mod/resource/services/handlers/prefetch.ts b/src/addons/mod/resource/services/handlers/prefetch.ts index 15886949e..f93df9fac 100644 --- a/src/addons/mod/resource/services/handlers/prefetch.ts +++ b/src/addons/mod/resource/services/handlers/prefetch.ts @@ -64,7 +64,7 @@ export class AddonModResourcePrefetchHandlerService extends CoreCourseResourcePr dirPath = await CoreFilepool.getPackageDirPathByUrl(CoreSites.getCurrentSiteId(), module.url!); } - const promises: Promise[] = []; + const promises: Promise[] = []; promises.push(super.downloadOrPrefetch(module, courseId, prefetch, dirPath)); diff --git a/src/addons/remotethemes/services/remotethemes.ts b/src/addons/remotethemes/services/remotethemes.ts index eea64fc8e..6dc82e1e4 100644 --- a/src/addons/remotethemes/services/remotethemes.ts +++ b/src/addons/remotethemes/services/remotethemes.ts @@ -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. diff --git a/src/assets/icon/icon.png b/src/assets/icon/icon.png new file mode 100644 index 000000000..f41b1f803 Binary files /dev/null and b/src/assets/icon/icon.png differ diff --git a/src/assets/img/group-avatar.png b/src/assets/img/group-avatar.png new file mode 100644 index 000000000..2e336c95c Binary files /dev/null and b/src/assets/img/group-avatar.png differ diff --git a/src/core/classes/site.ts b/src/core/classes/site.ts index 549ac2dcf..7fe336b26 100644 --- a/src/core/classes/site.ts +++ b/src/core/classes/site.ts @@ -560,7 +560,7 @@ export class CoreSite { return CoreUtils.clone(response); } - const promise = this.getFromCache(method, data, preSets, false).catch(() => { + const promise = this.getFromCache(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(method, data, preSets, wsPreSets).then((response) => { + try { + const response = await this.callOrEnqueueRequest(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(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(method, data, preSets, true)); + await CoreApp.waitForSSOAuthentication(); + + return this.request(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(method, data, preSets, true).catch(() => { + try { + return await this.getFromCache(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. diff --git a/src/core/constants.ts b/src/core/constants.ts index 7e6f45e98..8330e31d9 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -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; diff --git a/src/core/directives/supress-events.ts b/src/core/directives/supress-events.ts index 6a4ea4d35..e651b1eca 100644 --- a/src/core/directives/supress-events.ts +++ b/src/core/directives/supress-events.ts @@ -31,7 +31,7 @@ import { Directive, ElementRef, OnInit, Input, Output, EventEmitter } from '@ang * * Example usage: * - * + * */ @Directive({ selector: '[core-suppress-events]', diff --git a/src/core/features/block/components/block/block.ts b/src/core/features/block/components/block/block.ts index 05ee91cc0..2fe6f2a44 100644 --- a/src/core/features/block/components/block/block.ts +++ b/src/core/features/block/components/block/block.ts @@ -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; // Any extra data to be passed to the block. componentClass?: Type; // The class of the component to render. - data: any = {}; // Data to pass to the component. + data: Record = {}; // Data to pass to the component. class?: string; // CSS class to apply to the block. loaded = false; diff --git a/src/core/features/courses/components/course-progress/course-progress.ts b/src/core/features/courses/components/course-progress/course-progress.ts index dff808d28..75f0b9ce3 100644 --- a/src/core/features/courses/components/course-progress/course-progress.ts +++ b/src/core/features/courses/components/course-progress/course-progress.ts @@ -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 { + 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); } - }*/ + } } /** diff --git a/src/core/features/login/pages/reconnect/reconnect.ts b/src/core/features/login/pages/reconnect/reconnect.ts index 275b52e71..8eeb846b1 100644 --- a/src/core/features/login/pages/reconnect/reconnect.ts +++ b/src/core/features/login/pages/reconnect/reconnect.ts @@ -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')) { diff --git a/src/core/features/login/pages/site/site.html b/src/core/features/login/pages/site/site.html index dd52720d9..60c8a4458 100644 --- a/src/core/features/login/pages/site/site.html +++ b/src/core/features/login/pages/site/site.html @@ -63,7 +63,7 @@ - + @@ -118,7 +118,7 @@ - + diff --git a/src/core/features/login/pages/site/site.ts b/src/core/features/login/pages/site/site.ts index 0bbdc4939..7f94f447c 100644 --- a/src/core/features/login/pages/site/site.ts +++ b/src/core/features/login/pages/site/site.ts @@ -348,32 +348,36 @@ export class CoreLoginSitePage implements OnInit { * @return Promise resolved after logging in. */ protected async login(response: CoreSiteCheckResponse, foundSite?: CoreLoginSiteInfoExtended): Promise { - 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. diff --git a/src/core/features/login/services/handlers/cron.ts b/src/core/features/login/services/handlers/cron.ts index 826306c60..2add95869 100644 --- a/src/core/features/login/services/handlers/cron.ts +++ b/src/core/features/login/services/handlers/cron.ts @@ -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(), > {}); + const config = await CoreUtils.ignoreErrors(site.getPublicConfig()); - CoreUtils.ignoreErrors(CoreSites.checkApplication( config)); + CoreUtils.ignoreErrors(CoreSites.checkApplication(config)); } /** diff --git a/src/core/features/pushnotifications/services/pushnotifications.ts b/src/core/features/pushnotifications/services/pushnotifications.ts index 40090c253..5460402fa 100644 --- a/src/core/features/pushnotifications/services/pushnotifications.ts +++ b/src/core/features/pushnotifications/services/pushnotifications.ts @@ -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, diff --git a/src/core/services/navigator.ts b/src/core/services/navigator.ts index c5e380113..d543aac00 100644 --- a/src/core/services/navigator.ts +++ b/src/core/services/navigator.ts @@ -309,6 +309,7 @@ export class CoreNavigatorService { * @return Value of the parameter, undefined if not found. */ getRouteParam(name: string, routeOptions: CoreNavigatorCurrentRouteOptions = {}): T | undefined { + // eslint-disable-next-line @typescript-eslint/no-explicit-any let value: any; if (!routeOptions.params) { diff --git a/src/core/services/sites-factory.ts b/src/core/services/sites-factory.ts new file mode 100644 index 000000000..395494ea8 --- /dev/null +++ b/src/core/services/sites-factory.ts @@ -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); diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index f2385dd70..21f4eea6c 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -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('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 { - await this.checkRequiredMinimumVersion(response.config); + async checkApplication(config?: CoreSitePublicConfigResponse): Promise { + 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 { + protected async checkRequiredMinimumVersion(config?: CoreSitePublicConfigResponse): Promise { 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 ? CoreTextUtils.parseJSON(entry.info) : undefined; const config = entry.config ? 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 { - let siteId: string | undefined; + if (!this.currentSite) { + return; + } + + const db = await this.appDB; + const promises: Promise[] = []; + 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 { + async updateSiteInfo(siteId?: string): Promise { 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 { - const temporarySite = new CoreSite(undefined, siteUrl); + const temporarySite = CoreSitesFactory.makeSite(undefined, siteUrl); return temporarySite.getPublicConfig(); } diff --git a/src/core/services/urlschemes.ts b/src/core/services/urlschemes.ts index c5f40582d..7cbe49670 100644 --- a/src/core/services/urlschemes.ts +++ b/src/core/services/urlschemes.ts @@ -66,7 +66,7 @@ export class CoreCustomURLSchemesProvider { data.siteUrl = result.siteUrl; - await CoreSites.checkApplication(result); + await CoreSites.checkApplication(result.config); } return CoreSites.newSite( diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 1d267b7ce..75e68885f 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -1491,7 +1491,7 @@ export class CoreUtilsProvider { debounce(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); diff --git a/src/core/singletons/logger.ts b/src/core/singletons/logger.ts index 88e1dd93d..be7209703 100644 --- a/src/core/singletons/logger.ts +++ b/src/core/singletons/logger.ts @@ -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 diff --git a/src/core/singletons/math.ts b/src/core/singletons/math.ts new file mode 100644 index 000000000..1c32c947f --- /dev/null +++ b/src/core/singletons/math.ts @@ -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); + } + +} diff --git a/src/core/singletons/object.ts b/src/core/singletons/object.ts index 8f6bf1fa3..0b3dbbc38 100644 --- a/src/core/singletons/object.ts +++ b/src/core/singletons/object.ts @@ -21,6 +21,17 @@ export type CoreObjectWithoutEmpty = { */ 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. * diff --git a/src/core/singletons/time.ts b/src/core/singletons/time.ts new file mode 100644 index 000000000..8e8f92018 --- /dev/null +++ b/src/core/singletons/time.ts @@ -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(fn: (...args: T) => unknown): (...args: T) => void { + let called = false; + + return (...args: T) => { + if (called) { + return; + } + + called = true; + fn.apply(null, args); + }; + } + +} diff --git a/src/theme/globals.variables.scss b/src/theme/globals.variables.scss index c0c8e0eb4..327dd8f23 100644 --- a/src/theme/globals.variables.scss +++ b/src/theme/globals.variables.scss @@ -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; diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index 1e33ab0ef..a3b41bb81 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -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; diff --git a/src/theme/theme.light.scss b/src/theme/theme.light.scss index 1e7ca7425..13f04e715 100644 --- a/src/theme/theme.light.scss +++ b/src/theme/theme.light.scss @@ -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); } diff --git a/src/types/angular.d.ts b/src/types/angular.d.ts index ab158dc8c..4f63bb656 100644 --- a/src/types/angular.d.ts +++ b/src/types/angular.d.ts @@ -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; } diff --git a/upgrade.txt b/upgrade.txt index 0f6b475e9..4ee9ede3e 100644 --- a/upgrade.txt +++ b/upgrade.txt @@ -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 ===