From bfe9100879b6475afa56dab8c27d29060befa60b Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 19 Apr 2023 12:56:01 +0200 Subject: [PATCH 1/3] MOBILE-4290 core: Make waitForImages cancellable --- src/core/services/utils/dom.ts | 69 ++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 841a8195f..7a077c83b 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -59,6 +59,7 @@ import { CoreUserSupport } from '@features/user/services/support'; import { CoreErrorInfoComponent } from '@components/error-info/error-info'; import { CorePlatform } from '@services/platform'; import { AddonFilterMultilang2Handler } from '@addons/filter/multilang2/services/handlers/multilang2'; +import { CoreCancellablePromise } from '@classes/cancellable-promise'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -1918,32 +1919,62 @@ export class CoreDomUtilsProvider { * @param element The element to search in. * @returns Promise resolved with a boolean: whether there was any image to load. */ - async waitForImages(element: HTMLElement): Promise { + waitForImages(element: HTMLElement): CoreCancellablePromise { const imgs = Array.from(element.querySelectorAll('img')); - const promises: Promise[] = []; - let hasImgToLoad = false; - imgs.forEach((img) => { - if (img && !img.complete) { - hasImgToLoad = true; + if (imgs.length === 0) { + return CoreCancellablePromise.resolve(false); + } - // Wait for image to load or fail. - promises.push(new Promise((resolve) => { - const imgLoaded = (): void => { - resolve(); - img.removeEventListener('load', imgLoaded); - img.removeEventListener('error', imgLoaded); + let completedImages = 0; + let waitedForImages = false; + const listeners: WeakMap unknown> = new WeakMap(); + const imageCompleted = (resolve: (result: boolean) => void) => { + completedImages++; + + if (completedImages === imgs.length) { + resolve(waitedForImages); + } + }; + + return new CoreCancellablePromise( + resolve => { + for (const img of imgs) { + if (!img || img.complete) { + imageCompleted(resolve); + + continue; + } + + waitedForImages = true; + + // Wait for image to load or fail. + const imgCompleted = (): void => { + img.removeEventListener('load', imgCompleted); + img.removeEventListener('error', imgCompleted); + + imageCompleted(resolve); }; - img.addEventListener('load', imgLoaded); - img.addEventListener('error', imgLoaded); - })); - } - }); + img.addEventListener('load', imgCompleted); + img.addEventListener('error', imgCompleted); - await Promise.all(promises); + listeners.set(img, imgCompleted); + } + }, + () => { + imgs.forEach(img => { + const listener = listeners.get(img); - return hasImgToLoad; + if (!listener) { + return; + } + + img.removeEventListener('load', listener); + img.removeEventListener('error', listener); + }); + }, + ); } /** From b38248658095298c4c7cedf316feeab24f612752 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 19 Apr 2023 13:05:18 +0200 Subject: [PATCH 2/3] MOBILE-4290 login: Collapse site help questions --- .../login/components/site-help/site-help.html | 74 ++----- .../login/components/site-help/site-help.scss | 45 ++++- .../login/components/site-help/site-help.ts | 186 ++++++++++++++++-- src/core/services/utils/utils.ts | 29 +++ 4 files changed, 259 insertions(+), 75 deletions(-) diff --git a/src/core/features/login/components/site-help/site-help.html b/src/core/features/login/components/site-help/site-help.html index cb711026d..949754fd8 100644 --- a/src/core/features/login/components/site-help/site-help.html +++ b/src/core/features/login/components/site-help/site-help.html @@ -4,7 +4,7 @@

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

- + @@ -12,58 +12,26 @@ - - -

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

-
-
- - -

-
-
- - -

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

-
-
- - -

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

-
-
- - -

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

-
-
- - -

- + + + + +

{{ question.text }}

+
+
+ + +

{{ question.answer.text }}

+

+ -

-
-
- - -

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

-
-
- - -

-
-
- - -

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

-
-
- + + +
diff --git a/src/core/features/login/components/site-help/site-help.scss b/src/core/features/login/components/site-help/site-help.scss index c3eb78ce7..b92178674 100644 --- a/src/core/features/login/components/site-help/site-help.scss +++ b/src/core/features/login/components/site-help/site-help.scss @@ -1,13 +1,38 @@ -.core-login-faqwhatisurlanswer img { - max-height: 50px; -} +:host { -.core-login-faqwhereisqrcodeanswer img { - max-height: 220px; - margin-top: 5px; - margin-bottom: 5px; -} + .core-login-faqwhatisurlanswer img { + max-height: 50px; + } + + .core-login-faqwhereisqrcodeanswer img { + max-height: 220px; + margin-top: 5px; + margin-bottom: 5px; + } + + &:not(.hydrated) { + + .core-login-site-help--answer { + opacity: 0; + max-width: 100%; + position: absolute; + pointer-events: none; + } + + } + + &.hydrated { + + .core-login-site-help--answer { + height: 0; + transition: height 200ms ease-in-out; + + &.open { + height: var(--height); + } + + } + + } -h2 { - font-weight: bold; } diff --git a/src/core/features/login/components/site-help/site-help.ts b/src/core/features/login/components/site-help/site-help.ts index 35cf48cd6..4a3b22a91 100644 --- a/src/core/features/login/components/site-help/site-help.ts +++ b/src/core/features/login/components/site-help/site-help.ts @@ -12,11 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, HostBinding, OnDestroy } from '@angular/core'; import { CoreUtils } from '@services/utils/utils'; import { ModalController, Translate } from '@singletons'; import { CoreLoginHelperProvider, GET_STARTED_URL } from '@features/login/services/login-helper'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreCancellablePromise } from '@classes/cancellable-promise'; /** * Component that displays help to connect to a site. @@ -26,27 +28,187 @@ import { CoreLoginHelperProvider, GET_STARTED_URL } from '@features/login/servic templateUrl: 'site-help.html', styleUrls: ['site-help.scss'], }) -export class CoreLoginSiteHelpComponent { +export class CoreLoginSiteHelpComponent implements AfterViewInit, OnDestroy { - urlImageHtml: string; - setupLinkHtml: string; - qrCodeImageHtml: string; - canScanQR: boolean; + openQuestion?: number; + questions: Question[] = []; + @HostBinding('class.hydrated') hydrated = false; - constructor() { + private promises: CoreCancellablePromise[] = []; + + constructor(protected el: ElementRef) { const getStartedTitle = Translate.instant('core.login.faqsetupsitelinktitle'); + const canScanQR = !CoreUtils.canScanQR(); + const urlImageHtml = CoreLoginHelperProvider.FAQ_URL_IMAGE_HTML; + const qrCodeImageHtml = CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML; + const setupLinkHtml = `${GET_STARTED_URL}`; + const questions: Array = [ + { + text: Translate.instant('core.login.faqwhatisurlquestion'), + answer: { + text: Translate.instant('core.login.faqwhatisurlanswer', { $image: urlImageHtml }), + format: AnswerFormat.SafeHTML, + class: 'core-login-faqwhatisurlanswer', + }, + }, + { + text: Translate.instant('core.login.faqcannotfindmysitequestion'), + answer: { + text: Translate.instant('core.login.faqcannotfindmysiteanswer'), + format: AnswerFormat.Text, + }, + }, + { + text: Translate.instant('core.login.faqsetupsitequestion'), + answer: { + text: Translate.instant('core.login.faqsetupsiteanswer', { $link: setupLinkHtml }), + format: AnswerFormat.UnsafeHTML, + }, + }, + { + text: Translate.instant('core.login.faqtestappquestion'), + answer: { + text: Translate.instant('core.login.faqtestappanswer'), + format: AnswerFormat.SafeHTML, + }, + }, + canScanQR && { + text: Translate.instant('core.login.faqwhereisqrcode'), + answer: { + text: Translate.instant('core.login.faqwhereisqrcodeanswer', { $image: qrCodeImageHtml }), + format: AnswerFormat.SafeHTML, + class: 'core-login-faqwhereisqrcodeanswer', + }, + }, + ]; - this.canScanQR = CoreUtils.canScanQR(); - this.urlImageHtml = CoreLoginHelperProvider.FAQ_URL_IMAGE_HTML; - this.qrCodeImageHtml = CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML; - this.setupLinkHtml = `${GET_STARTED_URL}`; + for (const question of questions) { + if (!question) { + continue; + } + + this.questions.push({ + ...question, + id: this.questions.length + 1, + answer: { + ...question.answer, + class: question.answer.class ?? '', + }, + }); + } + } + + /** + * @inheritdoc + */ + async ngAfterViewInit(): Promise { + const answers = Array.from(this.el.nativeElement.querySelectorAll('.core-login-site-help--answer')); + + await Promise.all(answers.map(async answer => { + await this.track(CoreUtils.waitFor(() => answer.clientHeight !== 0)); + await this.track(CoreDomUtils.waitForImages(answer)); + + answer.style.setProperty('--height', `${answer.clientHeight}px`); + })); + + this.hydrated = true; + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.promises.forEach(promise => promise.cancel()); + } + + /** + * Check whether the given question is open or not. + * + * @param question Question. + * @returns Whether the given question is open. + */ + isOpen(question: Question): boolean { + return this.openQuestion === question.id; + } + + /** + * Toggle question. + * + * @param question Question to toggle. + */ + toggle(question: Question): void { + if (question.id === this.openQuestion) { + delete this.openQuestion; + + return; + } + + this.openQuestion = question.id; } /** * Close help modal. */ - closeHelp(): void { + close(): void { ModalController.dismiss(); } + /** + * Track a promise for cleanup. + * + * @param promise Cancellable promise. + * @returns The promise. + */ + protected track(promise: CoreCancellablePromise): Promise { + const remove = () => { + const index = this.promises.indexOf(promise); + + if (index === -1) { + return; + } + + this.promises.splice(index, 1); + }; + + this.promises.push(promise); + + promise.then(remove).catch(remove); + + return promise; + } + } + +/** + * Question data. + */ +interface Question { + id: number; + text: string; + answer: Answer; +} + +/** + * Question answer. + */ +interface Answer { + text: string; + class: string; + format: AnswerFormat; +} + +/** + * Question answer format. + */ +enum AnswerFormat { + Text = 'text', + SafeHTML = 'safe-html', + UnsafeHTML = 'unsafe-html', +} + +/** + * Question definition. + */ +type QuestionDefinition = Omit & { + answer: Omit & Partial>; +}; diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 39ef612e6..6f71dd6ee 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -38,6 +38,7 @@ import { CorePlatform } from '@services/platform'; import { CoreErrorWithOptions } from '@classes/errors/errorwithtitle'; import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; +import { CoreCancellablePromise } from '@classes/cancellable-promise'; export type TreeNode = T & { children: TreeNode[] }; @@ -1795,6 +1796,34 @@ export class CoreUtilsProvider { return new Promise(resolve => setTimeout(resolve, milliseconds)); } + /** + * Wait until a given condition is met. + * + * @param condition Condition. + * @returns Cancellable promise. + */ + waitFor(condition: () => boolean, interval: number = 50): CoreCancellablePromise { + if (condition()) { + return CoreCancellablePromise.resolve(); + } + + let intervalId: number | undefined; + + return new CoreCancellablePromise( + async (resolve) => { + intervalId = window.setInterval(() => { + if (!condition()) { + return; + } + + resolve(); + window.clearInterval(intervalId); + }, interval); + }, + () => window.clearInterval(intervalId), + ); + } + /** * Wait until the next tick. * From 156f01b7439e974aa963e57764e77cb5882891be Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 19 Apr 2023 13:26:54 +0200 Subject: [PATCH 3/3] MOBILE-4290 login: Update help text --- scripts/langindex.json | 3 +++ .../features/login/components/site-help/site-help.html | 5 +++++ .../features/login/components/site-help/site-help.ts | 7 +++++++ src/core/features/login/lang.json | 9 ++++++--- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 64a23a98d..a2b5a037f 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -2045,6 +2045,9 @@ "core.login.exceededpasswordresetattemptssupportsubject": "local_moodlemobileapp", "core.login.faqcannotfindmysiteanswer": "local_moodlemobileapp", "core.login.faqcannotfindmysitequestion": "local_moodlemobileapp", + "core.login.faqcantloginanswer": "local_moodlemobileapp", + "core.login.faqcantloginquestion": "local_moodlemobileapp", + "core.login.faqmore": "local_moodlemobileapp", "core.login.faqsetupsiteanswer": "local_moodlemobileapp", "core.login.faqsetupsitelinktitle": "local_moodlemobileapp", "core.login.faqsetupsitequestion": "local_moodlemobileapp", diff --git a/src/core/features/login/components/site-help/site-help.html b/src/core/features/login/components/site-help/site-help.html index 949754fd8..d1cea07d2 100644 --- a/src/core/features/login/components/site-help/site-help.html +++ b/src/core/features/login/components/site-help/site-help.html @@ -33,5 +33,10 @@ + + +

+
+
diff --git a/src/core/features/login/components/site-help/site-help.ts b/src/core/features/login/components/site-help/site-help.ts index 4a3b22a91..c1bdf471c 100644 --- a/src/core/features/login/components/site-help/site-help.ts +++ b/src/core/features/login/components/site-help/site-help.ts @@ -58,6 +58,13 @@ export class CoreLoginSiteHelpComponent implements AfterViewInit, OnDestroy { format: AnswerFormat.Text, }, }, + { + text: Translate.instant('core.login.faqcantloginquestion'), + answer: { + text: Translate.instant('core.login.faqcantloginanswer'), + format: AnswerFormat.SafeHTML, + }, + }, { text: Translate.instant('core.login.faqsetupsitequestion'), answer: { diff --git a/src/core/features/login/lang.json b/src/core/features/login/lang.json index bcbffa477..faecfd43a 100644 --- a/src/core/features/login/lang.json +++ b/src/core/features/login/lang.json @@ -35,14 +35,17 @@ "exceededloginattemptswithoutsupport": "Need help logging in? Try {{recoverPassword}}.", "exceededpasswordresetattempts": "It seems you are having trouble accessing your account. You can contact your school or learning provider or try again later.", "exceededpasswordresetattemptssupportsubject": "I can't reset my password", - "faqcannotfindmysiteanswer": "If you still can't find it, please contact your school or learning provider for help.", - "faqcannotfindmysitequestion": "I can't find my site by address.", + "faqcannotfindmysiteanswer": "If you tried searching by URL address and still can't find your Moodle site, please get in touch with the person who takes care of Moodle in your school or learning organisation.", + "faqcannotfindmysitequestion": "I can't find my site by URL address.", + "faqcantloginanswer": "

Once you've connected to your Moodle site, you should be able to log in with your usual username and password.


If you forgot your username or password, select the option Forgotten your username or password?. If you still have trouble logging in or can't see any options for retrieving your username or password, please get in touch with the person who takes care of Moodle in your school or learning organisation.

", + "faqcantloginquestion": "I can't log in.", + "faqmore": "Check out our FAQ for more answers.", "faqsetupsiteanswer": "Visit {{$link}} to check out the different options you have to create your own Moodle site.", "faqsetupsitelinktitle": "Get started.", "faqsetupsitequestion": "I want to set up my own Moodle site.", "faqtestappanswer": "To test the app in a Moodle demo site, type \"teacher\" or \"student\" in the Your site field and tap Connect to your site.", "faqtestappquestion": "Can I test the app on a demo site?", - "faqwhatisurlanswer": "

If you can't find your site by name, try searching by address instead.

To find your site address:

  1. Open a web browser and go to your Moodle site login page.
  2. At the top of the page, in the address bar, you will see the URL of your Moodle site e.g. \"campus.example.edu\".
    {{$image}}
  3. Copy the address (don't copy the /login nor what comes after), paste it into Your site in the app, then tap Connect to your site
  4. Now you can log in to your site with your username and password.
", + "faqwhatisurlanswer": "

If you can't find your site by name, try searching by your Moodle site URL address (or web address) address instead.

To find your site address:

  1. Open a web browser and go to your Moodle site login page.
  2. At the top of the page, in the address bar, you'll see the URL address of your Moodle site, e.g. \"campus.example.edu\".
    {{$image}}
  3. Copy the URL (don't copy the /login nor what comes after), paste it into Your site in the app, then tap Connect to your site
  4. Now you can log in to your site with your username and password.
", "faqwhatisurlquestion": "How can I find my site?", "faqwhereisqrcode": "Where can I find the QR code?", "faqwhereisqrcodeanswer": "

If your school or learning provider has enabled it, you will find a QR code on the web site at the bottom of your user profile page.

{{$image}}",