From 27ceabde629d4d7aa20334a214e7b870f250decd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 27 Apr 2020 13:43:13 +0200 Subject: [PATCH 1/3] MOBILE-3394 utils: Manage store links --- src/config.json | 9 +++- src/core/login/providers/helper.ts | 28 +++------- src/providers/app.ts | 83 ++++++++++++++++++++++++++++++ src/providers/sites.ts | 22 ++++---- 4 files changed, 107 insertions(+), 35 deletions(-) diff --git a/src/config.json b/src/config.json index 245bd890e..71864677a 100644 --- a/src/config.json +++ b/src/config.json @@ -94,5 +94,12 @@ "statusbarlighttextremotetheme": true, "enableanalytics": false, "forceColorScheme": "", - "webviewscheme": "moodleappfs" + "webviewscheme": "moodleappfs", + "appstores": { + "android": "com.moodle.moodlemobile", + "ios": "id633359593", + "windows": "moodle-desktop/9p9bwvhdc8c8", + "mac": "id1255924440", + "linux": "https://download.moodle.org/desktop/download.php?platform=linux&arch=64" + } } diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index 1bd6a43f6..c904be356 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { Location } from '@angular/common'; import { Platform, AlertController, NavController, NavOptions } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreAppProvider } from '@providers/app'; +import { CoreAppProvider, CoreStoreConfig } from '@providers/app'; import { CoreConfigProvider } from '@providers/config'; import { CoreEventsProvider } from '@providers/events'; import { CoreInitDelegate } from '@providers/init'; @@ -1180,13 +1180,7 @@ export class CoreLoginHelperProvider { * @param message The warning message. */ protected showWorkplaceNoticeModal(message: string): void { - let link; - - if (this.platform.is('android')) { - link = 'market://details?id=com.moodle.workplace'; - } else if (this.platform.is('ios')) { - link = 'itms-apps://itunes.apple.com/app/id1470929705'; - } + const link = this.appProvider.getAppStoreUrl({android: 'com.moodle.workplace', ios: 'id1470929705' }); this.showDownloadAppNoticeModal(message, link); } @@ -1197,20 +1191,12 @@ export class CoreLoginHelperProvider { * @param message The warning message. */ protected showMoodleAppNoticeModal(message: string): void { - let link; + const storesConfig: CoreStoreConfig = CoreConfigConstants.appstores; + storesConfig.desktop = 'https://download.moodle.org/desktop/'; + storesConfig.mobile = 'https://download.moodle.org/mobile/'; + storesConfig.default = 'https://download.moodle.org/mobile/'; - if (this.appProvider.isWindows()) { - link = 'https://download.moodle.org/desktop/download.php?platform=windows'; - } else if (this.appProvider.isLinux()) { - link = 'https://download.moodle.org/desktop/download.php?platform=linux&arch=' + - (this.appProvider.is64Bits() ? '64' : '32'); - } else if (this.appProvider.isMac()) { - link = 'itms-apps://itunes.apple.com/app/id1255924440'; - } else if (this.platform.is('android')) { - link = 'market://details?id=com.moodle.moodlemobile'; - } else if (this.platform.is('ios')) { - link = 'itms-apps://itunes.apple.com/app/id633359593'; - } + const link = this.appProvider.getAppStoreUrl(storesConfig); this.showDownloadAppNoticeModal(message, link); } diff --git a/src/providers/app.ts b/src/providers/app.ts index 4d29185e5..8f0625765 100644 --- a/src/providers/app.ts +++ b/src/providers/app.ts @@ -50,6 +50,51 @@ export interface CoreRedirectData { timemodified?: number; } +/** + * Store config data. + */ +export interface CoreStoreConfig { + /** + * ID of the Apple store where the desktop Mac app is uploaded. + */ + mac?: string; + + /** + * ID of the Windows store where the desktop Windows app is uploaded. + */ + windows?: string; + + /** + * Url with the desktop linux download link. + */ + linux?: string; + + /** + * Fallback URL when the desktop options is not set. + */ + desktop?: string; + + /** + * ID of the Apple store where the mobile iOS app is uploaded. + */ + ios?: string; + + /** + * ID of the Google play store where the android app is uploaded. + */ + android?: string; + + /** + * Fallback URL when the mobile options is not set. + */ + mobile?: string; + + /** + * Fallback URL when the other fallbacks options are not set. + */ + default?: string; +} + /** * App DB schema and migration function. */ @@ -255,6 +300,44 @@ export class CoreAppProvider { return this.appCtrl.getRootNavs()[0]; } + /** + * Get app store URL. + * + * @param storesConfig Config params to send the user to the right place. + * @return Store URL. + */ + getAppStoreUrl(storesConfig: CoreStoreConfig): string { + if (this.isMac() && storesConfig.mac) { + return 'itms-apps://itunes.apple.com/app/' + storesConfig.mac; + } + + if (this.isWindows() && storesConfig.windows) { + return 'https://www.microsoft.com/p/' + storesConfig.windows; + } + + if (this.isLinux() && storesConfig.linux) { + return storesConfig.linux; + } + + if (this.isDesktop() && storesConfig.desktop) { + return storesConfig.desktop; + } + + if (this.isIOS() && storesConfig.ios) { + return 'itms-apps://itunes.apple.com/app/' + storesConfig.ios; + } + + if (this.isAndroid() && storesConfig.android) { + return 'market://details?id=' + storesConfig.android; + } + + if (this.isMobile() && storesConfig.mobile) { + return storesConfig.mobile; + } + + return storesConfig.default || null; + } + /** * Returns whether the user agent is controlled by automation. I.e. Behat testing. * diff --git a/src/providers/sites.ts b/src/providers/sites.ts index 5eabb68c0..df553e7d1 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -15,7 +15,7 @@ import { Injectable, Injector } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; -import { CoreAppProvider, CoreAppSchema } from './app'; +import { CoreAppProvider, CoreAppSchema, CoreStoreConfig } from './app'; import { CoreEventsProvider } from './events'; import { CoreLoggerProvider } from './logger'; import { CoreSitesFactoryProvider } from './sites-factory'; @@ -1001,19 +1001,15 @@ export class CoreSitesProvider { appVersion = this.convertVersionName(CoreConfigConstants.versionname); if (requiredVersion > appVersion) { - let downloadUrl = ''; + const storesConfig: CoreStoreConfig = { + android: config.tool_mobile_androidappid || false, + ios: config.tool_mobile_iosappid || false, + desktop: config.tool_mobile_setuplink || 'https://download.moodle.org/desktop/', + mobile: config.tool_mobile_setuplink || 'https://download.moodle.org/mobile/', + default: config.tool_mobile_setuplink, + }; - if (this.appProvider.isAndroid() && config.tool_mobile_androidappid) { - downloadUrl = 'market://details?id=' + config.tool_mobile_androidappid; - } else if (this.appProvider.isIOS() && config.tool_mobile_iosappid) { - downloadUrl = 'itms-apps://itunes.apple.com/app/id' + config.tool_mobile_iosappid; - } else if (config.tool_mobile_setuplink) { - downloadUrl = config.tool_mobile_setuplink; - } else if (this.appProvider.isMobile()) { - downloadUrl = 'https://download.moodle.org/mobile/'; - } else { - downloadUrl = 'https://download.moodle.org/desktop/'; - } + const downloadUrl = this.appProvider.getAppStoreUrl(storesConfig); siteId = siteId || this.getCurrentSiteId(); From f3e5c21b6e017217b458b7494e3e0e2abe751cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 27 Apr 2020 13:43:38 +0200 Subject: [PATCH 2/3] MOBILE-3394 utils: Improve alerts and prompts --- src/core/login/pages/site/site.ts | 19 +- src/core/viewer/pages/textarea/textarea.html | 14 ++ .../viewer/pages/textarea/textarea.module.ts | 36 +++ src/core/viewer/pages/textarea/textarea.scss | 187 ++++++++++++++++ src/core/viewer/pages/textarea/textarea.ts | 71 ++++++ src/providers/utils/dom.ts | 208 +++++++++--------- 6 files changed, 421 insertions(+), 114 deletions(-) create mode 100644 src/core/viewer/pages/textarea/textarea.html create mode 100644 src/core/viewer/pages/textarea/textarea.module.ts create mode 100644 src/core/viewer/pages/textarea/textarea.scss create mode 100644 src/core/viewer/pages/textarea/textarea.ts diff --git a/src/core/login/pages/site/site.ts b/src/core/login/pages/site/site.ts index eec309a4c..220e055d3 100644 --- a/src/core/login/pages/site/site.ts +++ b/src/core/login/pages/site/site.ts @@ -277,7 +277,11 @@ export class CoreLoginSitePage { } ]; - this.domUtils.showAlertWithButtons(this.translate.instant('core.cannotconnect'), message, buttons); + this.domUtils.showAlertWithOptions({ + title: this.translate.instant('core.cannotconnect'), + message, + buttons, + }); } /** @@ -369,10 +373,11 @@ export class CoreLoginSitePage { */ showInstructionsAndScanQR(): void { // Show some instructions first. - this.domUtils.showAlertWithButtons( - this.translate.instant('core.login.faqwhereisqrcode'), - this.translate.instant('core.login.faqwhereisqrcodeanswer', {$image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML}), - [ + this.domUtils.showAlertWithOptions({ + title: this.translate.instant('core.login.faqwhereisqrcode'), + message: this.translate.instant('core.login.faqwhereisqrcodeanswer', + {$image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML}), + buttons: [ { text: this.translate.instant('core.cancel'), role: 'cancel' @@ -383,8 +388,8 @@ export class CoreLoginSitePage { this.scanQR(); } }, - ] - ); + ], + }); } /** diff --git a/src/core/viewer/pages/textarea/textarea.html b/src/core/viewer/pages/textarea/textarea.html new file mode 100644 index 000000000..d43f4a11e --- /dev/null +++ b/src/core/viewer/pages/textarea/textarea.html @@ -0,0 +1,14 @@ + + + {{ title }} + + + +
+ +
+ +
+
diff --git a/src/core/viewer/pages/textarea/textarea.module.ts b/src/core/viewer/pages/textarea/textarea.module.ts new file mode 100644 index 000000000..a17d8cc8e --- /dev/null +++ b/src/core/viewer/pages/textarea/textarea.module.ts @@ -0,0 +1,36 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreViewerTextAreaPage } from './textarea'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +/** + * Module to lazy load the page. + */ +@NgModule({ + declarations: [ + CoreViewerTextAreaPage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(CoreViewerTextAreaPage), + TranslateModule.forChild() + ] +}) +export class CoreViewerTextAreaPageModule {} diff --git a/src/core/viewer/pages/textarea/textarea.scss b/src/core/viewer/pages/textarea/textarea.scss new file mode 100644 index 000000000..27a2ea061 --- /dev/null +++ b/src/core/viewer/pages/textarea/textarea.scss @@ -0,0 +1,187 @@ +$core-modal-promt-min-width: 320px; + +ion-app.app-root ion-modal.core-modal-prompt { + /* Some styles have been copied from ionic alert component. */ + @include position(0, 0, 0, 0); + position: absolute; + z-index: $z-index-overlay; + display: flex; + align-items: center; + justify-content: center; + contain: strict; + + ion-backdrop { + visibility: visible; + } + + .header { + &::after { + background: none; + } + .toolbar-background { + display: none; + } + } + + .prompt-button-group { + display: flex; + flex-direction: row; + + .prompt-button { + @include margin(0); + + z-index: 0; + display: block; + + font-size: $alert-button-font-size; + line-height: $alert-button-line-height; + } + } + + ion-textarea { + @include placeholder($alert-input-placeholder-color); + @include padding($alert-md-message-padding-top, $alert-md-message-padding-end, $alert-md-message-padding-bottom, $alert-md-message-padding-start); + border: 0; + background: inherit; + + textarea { + margin: 0; + } + } + + .prompt-message { + @include deprecated-variable(padding, $alert-md-message-padding) { + @include padding($alert-md-message-padding-top, $alert-md-message-padding-end, $alert-md-message-padding-bottom, $alert-md-message-padding-start); + } + } + + .modal-wrapper { + z-index: $z-index-overlay-wrapper; + display: flex; + flex-direction: column; + min-width: $core-modal-promt-min-width; + max-height: $alert-max-height; + opacity: 0; + contain: content; + height: auto; + + page-core-viewer-textarea, + ion-content, + .fixed-content, + .scroll-content { + position: relative; + background: $white; + overflow: hidden; + } + .fixed-content { + display: none; + } + .scroll-content { + padding: 0 !important; + } + } + + .content-md .prompt-button-group { + flex-wrap: $alert-md-button-group-flex-wrap; + justify-content: $alert-md-button-group-justify-content; + + @include deprecated-variable(padding, $alert-md-button-group-padding) { + @include padding($alert-md-button-group-padding-top, $alert-md-button-group-padding-end, $alert-md-button-group-padding-bottom, $alert-md-button-group-padding-start); + } + + .prompt-button { + @include text-align($alert-md-button-text-align); + @include border-radius($alert-md-button-border-radius); + + // necessary for ripple to work properly + position: relative; + overflow: hidden; + + font-weight: $alert-md-button-font-weight; + text-transform: $alert-md-button-text-transform; + color: $alert-md-button-text-color; + background-color: $alert-md-button-background-color; + + @include deprecated-variable(margin, $alert-md-button-margin) { + @include margin($alert-md-button-margin-top, $alert-md-button-margin-end, $alert-md-button-margin-bottom, $alert-md-button-margin-start); + } + + @include deprecated-variable(padding, $alert-md-button-padding) { + @include padding($alert-md-button-padding-top, $alert-md-button-padding-end, $alert-md-button-padding-bottom, $alert-md-button-padding-start); + } + } + + .prompt-button.activated { + background-color: $alert-md-button-background-color-activated; + } + + .prompt-button .button-inner { + justify-content: $alert-md-button-group-justify-content; + } + } + + .content-ios .prompt-button-group { + @include margin-horizontal(null, -$alert-ios-button-border-width); + + flex-wrap: $alert-ios-button-group-flex-wrap; + .prompt-button { + @include margin($alert-ios-button-margin); + @include border-radius($alert-ios-button-border-radius); + + overflow: hidden; + + flex: $alert-ios-button-flex; + + min-width: $alert-ios-button-min-width; + height: $alert-ios-button-min-height; + + border-top: $alert-ios-button-border-width $alert-ios-button-border-style $alert-ios-button-border-color; + border-right: $alert-ios-button-border-width $alert-ios-button-border-style $alert-ios-button-border-color; + font-size: $alert-ios-button-font-size; + color: $alert-ios-button-text-color; + background-color: $alert-ios-button-background-color; + } + + .prompt-button:last-child { + border-right: 0; + font-weight: $alert-ios-button-main-font-weight; + } + + .prompt-button.activated { + background-color: $alert-ios-button-background-color-activated; + } + } +} + +ion-app.app-root-md ion-modal.core-modal-prompt { + .modal-wrapper { + @include border-radius($alert-md-border-radius); + max-width: $alert-md-max-width; + background-color: $alert-md-background-color; + box-shadow: $alert-md-box-shadow; + } + + .toolbar-content .toolbar-title { + color: $alert-md-message-text-color; + white-space: normal; + } +} + +ion-app.app-root-ios ion-modal.core-modal-prompt { + .modal-wrapper { + @include border-radius($alert-ios-border-radius); + overflow: hidden; + max-width: $alert-ios-max-width; + background-color: $alert-ios-background; + box-shadow: $alert-ios-box-shadow; + } + + .toolbar-content .toolbar-title { + color: $alert-ios-message-text-color; + white-space: normal; + } + + ion-title { + padding: 0; + } +} \ No newline at end of file diff --git a/src/core/viewer/pages/textarea/textarea.ts b/src/core/viewer/pages/textarea/textarea.ts new file mode 100644 index 000000000..e01e418be --- /dev/null +++ b/src/core/viewer/pages/textarea/textarea.ts @@ -0,0 +1,71 @@ +// (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 { Component } from '@angular/core'; +import { IonicPage, ViewController, NavParams, AlertButton } from 'ionic-angular'; + +/** + * Page to render a textarea prompt. + */ +@IonicPage({ segment: 'core-viewer-textarea' }) +@Component({ + selector: 'page-core-viewer-textarea', + templateUrl: 'textarea.html', +}) +export class CoreViewerTextAreaPage { + title: string; + message: string; + placeholder: string; + buttons: AlertButton[]; + text = ''; + + constructor( + protected viewCtrl: ViewController, + params: NavParams, + ) { + this.title = params.get('title'); + this.message = params.get('message'); + this.placeholder = params.get('placeholder') || ''; + + const buttons = params.get('buttons'); + + this.buttons = buttons.map((button) => { + if (typeof button === 'string') { + return { text: button }; + } + + return button; + }); + } + + /** + * Button clicked. + * + * @param button: Clicked button. + */ + buttonClicked(button: AlertButton): void { + let shouldDismiss = true; + if (button.handler) { + // A handler has been provided, execute it pass the handler the values from the inputs + if (button.handler(this.text) === false) { + // If the return value of the handler is false then do not dismiss + shouldDismiss = false; + } + } + + if (shouldDismiss) { + this.viewCtrl.dismiss(button.role); + } + } +} diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index b4c904f9a..9ef11f960 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -15,7 +15,7 @@ import { Injectable, SimpleChange, ElementRef } from '@angular/core'; import { LoadingController, Loading, ToastController, Toast, AlertController, Alert, Platform, Content, PopoverController, - ModalController, AlertButton + ModalController, AlertButton, AlertOptions } from 'ionic-angular'; import { DomSanitizer } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; @@ -1139,41 +1139,36 @@ export class CoreDomUtilsProvider { * @return Promise resolved with the alert modal. */ async showAlert(title: string, message: string, buttonText?: string, autocloseTime?: number): Promise { - const buttons = [buttonText || this.translate.instant('core.ok')]; - - return this.showAlertWithButtons(title, message, buttons, autocloseTime); + return this.showAlertWithOptions({ + title: title, + message, + buttons: [buttonText || this.translate.instant('core.ok')] + }, autocloseTime); } /** - * Show an alert modal with some buttons. + * General show an alert modal. * - * @param title Title to show. - * @param message Message to show. - * @param buttons Buttons objects or texts. + * @param options Alert options to pass to the alert. * @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. * @return Promise resolved with the alert modal. */ - async showAlertWithButtons(title: string, message: string, buttons: (string | AlertButton)[], autocloseTime?: number): - Promise { - const hasHTMLTags = this.textUtils.hasHTMLTags(message); + async showAlertWithOptions(options: AlertOptions = {}, autocloseTime?: number): Promise { + const hasHTMLTags = this.textUtils.hasHTMLTags(options.message || ''); if (hasHTMLTags) { // Format the text. - message = await this.textUtils.formatText(message); + options.message = await this.textUtils.formatText(options.message); } - const alertId = Md5.hashAsciiStr((title || '') + '#' + (message || '')); + const alertId = Md5.hashAsciiStr((options.title || '') + '#' + (options.message || '')); if (this.displayedAlerts[alertId]) { // There's already an alert with the same message and title. Return it. return this.displayedAlerts[alertId]; } - const alert: CoreAlert = this.alertCtrl.create({ - title: title, - message: message, - buttons: buttons, - }); + const alert: CoreAlert = this.alertCtrl.create(options); alert.present().then(() => { if (hasHTMLTags) { @@ -1202,8 +1197,18 @@ export class CoreDomUtilsProvider { }); if (autocloseTime > 0) { - setTimeout(() => { - alert.dismiss(); + setTimeout(async () => { + await alert.dismiss(); + + if (options.buttons) { + // Execute dismiss function if any. + const cancelButton = options.buttons.find((button) => { + return typeof button != 'string' && typeof button.role != 'undefined' && + typeof button.handler != 'undefined' && button.role == 'cancel'; + }); + cancelButton && cancelButton.handler(null); + } + }, autocloseTime); } @@ -1250,52 +1255,33 @@ export class CoreDomUtilsProvider { * @param options More options. See https://ionicframework.com/docs/v3/api/components/alert/AlertController/ * @return Promise resolved if the user confirms and rejected with a canceled error if he cancels. */ - showConfirm(message: string, title?: string, okText?: string, cancelText?: string, options?: any): Promise { + showConfirm(message: string, title?: string, okText?: string, cancelText?: string, options: AlertOptions = {}): Promise { return new Promise((resolve, reject): void => { - const hasHTMLTags = this.textUtils.hasHTMLTags(message); - let promise; - if (hasHTMLTags) { - // Format the text. - promise = this.textUtils.formatText(message); - } else { - promise = Promise.resolve(message); + options.title = title; + options.message = message; + + options.buttons = [ + { + text: cancelText || this.translate.instant('core.cancel'), + role: 'cancel', + handler: (): void => { + reject(this.createCanceledError()); + } + }, + { + text: okText || this.translate.instant('core.ok'), + handler: (data: any): void => { + resolve(data); + } + } + ]; + + if (!title) { + options.cssClass = 'core-nohead'; } - promise.then((message) => { - options = options || {}; - - options.message = message; - options.title = title; - if (!title) { - options.cssClass = 'core-nohead'; - } - options.buttons = [ - { - text: cancelText || this.translate.instant('core.cancel'), - role: 'cancel', - handler: (): void => { - reject(this.createCanceledError()); - } - }, - { - text: okText || this.translate.instant('core.ok'), - handler: (data: any): void => { - resolve(data); - } - } - ]; - - const alert = this.alertCtrl.create(options); - - alert.present().then(() => { - if (hasHTMLTags) { - // Treat all anchors so they don't override the app. - const alertMessageEl: HTMLElement = alert.pageRef().nativeElement.querySelector('.alert-message'); - this.treatAnchors(alertMessageEl); - } - }); - }); + this.showAlertWithOptions(options, 0); }); } @@ -1414,59 +1400,67 @@ export class CoreDomUtilsProvider { * @param title Modal title. * @param placeholder Placeholder of the input element. By default, "Password". * @param type Type of the input element. By default, password. + * @param options More options to pass to the alert. * @return Promise resolved with the input data if the user clicks OK, rejected if cancels. */ showPrompt(message: string, title?: string, placeholder?: string, type: string = 'password'): Promise { - return new Promise((resolve, reject): void => { - const hasHTMLTags = this.textUtils.hasHTMLTags(message); - let promise; + return new Promise((resolve, reject): any => { + placeholder = typeof placeholder == 'undefined' || placeholder == null ? + this.translate.instant('core.login.password') : placeholder; - if (hasHTMLTags) { - // Format the text. - promise = this.textUtils.formatText(message); - } else { - promise = Promise.resolve(message); - } - - promise.then((message) => { - const alert = this.alertCtrl.create({ - message: message, - title: title, - inputs: [ - { - name: 'promptinput', - placeholder: placeholder || this.translate.instant('core.login.password'), - type: type - } - ], - buttons: [ - { - text: this.translate.instant('core.cancel'), - role: 'cancel', - handler: (): void => { - reject(); - } - }, - { - text: this.translate.instant('core.ok'), - handler: (data): void => { - resolve(data.promptinput); - } - } - ] - }); - - alert.present().then(() => { - if (hasHTMLTags) { - // Treat all anchors so they don't override the app. - const alertMessageEl: HTMLElement = alert.pageRef().nativeElement.querySelector('.alert-message'); - this.treatAnchors(alertMessageEl); + const options: AlertOptions = { + title, + message, + inputs: [ + { + name: 'promptinput', + placeholder: placeholder, + type: type } - }); - }); + ], + buttons: [ + { + text: this.translate.instant('core.cancel'), + role: 'cancel', + handler: (): void => { + reject(); + } + }, + { + text: this.translate.instant('core.ok'), + handler: (data): void => { + resolve(data.promptinput); + } + } + ], + }; + + this.showAlertWithOptions(options); }); } + /** + * Show a prompt modal to input a textarea. + * + * @param title Modal title. + * @param message Modal message. + * @param buttons Buttons to pass to the modal. + * @param placeholder Placeholder of the input element if any. + * @return Promise resolved when modal presented. + */ + showTextareaPrompt(title: string, message: string, buttons: (string | AlertButton)[], placeholder?: string): Promise { + const params = { + title: title, + message: message, + placeholder: placeholder, + buttons: buttons, + }; + + const modal = this.modalCtrl.create('CoreViewerTextAreaPage', params, { cssClass: 'core-modal-prompt' }); + + return modal.present(); + } + /** * Displays an autodimissable toast modal window. * From 02f321433210bf62bc05096faf35915ae57450a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 27 Apr 2020 14:42:47 +0200 Subject: [PATCH 3/3] MOBILE-3394 mod: Add event when user activity is sent --- src/addon/mod/assign/pages/edit/edit.ts | 4 ++++ src/addon/mod/chat/pages/chat/chat.ts | 1 + src/addon/mod/choice/components/index/index.ts | 16 +++++++++++++--- src/addon/mod/data/pages/edit/edit.ts | 4 ++++ src/addon/mod/feedback/pages/form/form.ts | 2 ++ .../forum/pages/new-discussion/new-discussion.ts | 2 ++ src/addon/mod/glossary/pages/edit/edit.ts | 1 + src/addon/mod/lesson/pages/player/player.ts | 5 +++-- src/addon/mod/quiz/pages/player/player.ts | 2 ++ src/addon/mod/scorm/pages/player/player.ts | 2 ++ src/addon/mod/survey/components/index/index.ts | 14 +++++++++++--- src/addon/mod/wiki/pages/edit/edit.ts | 2 ++ .../pages/edit-submission/edit-submission.ts | 2 ++ src/providers/events.ts | 1 + 14 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/addon/mod/assign/pages/edit/edit.ts b/src/addon/mod/assign/pages/edit/edit.ts index 87eb9ee29..33fcf85b8 100644 --- a/src/addon/mod/assign/pages/edit/edit.ts +++ b/src/addon/mod/assign/pages/edit/edit.ts @@ -317,6 +317,10 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy { // Clear temporary data from plugins. await this.assignHelper.clearSubmissionPluginTmpData(this.assign, this.userSubmission, inputData); + if (sent) { + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'assign' }); + } + // Submission saved, trigger events. this.domUtils.triggerFormSubmittedEvent(this.formElement, sent, this.sitesProvider.getCurrentSiteId()); diff --git a/src/addon/mod/chat/pages/chat/chat.ts b/src/addon/mod/chat/pages/chat/chat.ts index bd34a6644..3130f397c 100644 --- a/src/addon/mod/chat/pages/chat/chat.ts +++ b/src/addon/mod/chat/pages/chat/chat.ts @@ -117,6 +117,7 @@ export class AddonModChatChatPage { * Runs when the page is about to leave and no longer be the active page. */ ionViewWillLeave(): void { + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'chat' }); this.stopPolling(); } diff --git a/src/addon/mod/choice/components/index/index.ts b/src/addon/mod/choice/components/index/index.ts index 6423f5439..2ce8f309b 100644 --- a/src/addon/mod/choice/components/index/index.ts +++ b/src/addon/mod/choice/components/index/index.ts @@ -14,6 +14,7 @@ import { Component, Optional, Injector } from '@angular/core'; import { Content } from 'ionic-angular'; +import { CoreEvents, CoreEventsProvider } from '@providers/events'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; import { AddonModChoiceProvider, AddonModChoiceChoice, AddonModChoiceOption, AddonModChoiceResult } from '../../providers/choice'; @@ -51,9 +52,14 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo protected hasAnsweredOnline = false; protected now: number; - constructor(injector: Injector, private choiceProvider: AddonModChoiceProvider, @Optional() content: Content, - private choiceOffline: AddonModChoiceOfflineProvider, private choiceSync: AddonModChoiceSyncProvider, - private timeUtils: CoreTimeUtilsProvider) { + constructor( + injector: Injector, + protected choiceProvider: AddonModChoiceProvider, + @Optional() content: Content, + protected choiceOffline: AddonModChoiceOfflineProvider, + protected choiceSync: AddonModChoiceSyncProvider, + protected timeUtils: CoreTimeUtilsProvider, + ) { super(injector, content); } @@ -359,6 +365,10 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo this.courseProvider.checkModuleCompletion(this.courseId, this.module.completiondata); this.domUtils.scrollToTop(this.content); + if (online) { + CoreEvents.instance.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: this.moduleName }); + } + return this.dataUpdated(online); }).catch((message) => { this.domUtils.showErrorModalDefault(message, 'addon.mod_choice.cannotsubmit', true); diff --git a/src/addon/mod/data/pages/edit/edit.ts b/src/addon/mod/data/pages/edit/edit.ts index a830e02d8..877785bbd 100644 --- a/src/addon/mod/data/pages/edit/edit.ts +++ b/src/addon/mod/data/pages/edit/edit.ts @@ -217,6 +217,10 @@ export class AddonModDataEditPage { this.domUtils.triggerFormSubmittedEvent(this.formElement, result.sent, this.siteId); + if (result.sent) { + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'data' }); + } + const promises = []; this.entryId = this.entryId || result.newentryid; diff --git a/src/addon/mod/feedback/pages/form/form.ts b/src/addon/mod/feedback/pages/form/form.ts index 3ee5f6ce9..8e136401a 100644 --- a/src/addon/mod/feedback/pages/form/form.ts +++ b/src/addon/mod/feedback/pages/form/form.ts @@ -280,6 +280,8 @@ export class AddonModFeedbackFormPage implements OnDestroy { promises.push(this.feedbackProvider.invalidateFeedbackAccessInformationData(this.feedback.id)); promises.push(this.feedbackProvider.invalidateResumePageData(this.feedback.id)); + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'feedback' }); + return Promise.all(promises).then(() => { return this.fetchAccessData(); }); diff --git a/src/addon/mod/forum/pages/new-discussion/new-discussion.ts b/src/addon/mod/forum/pages/new-discussion/new-discussion.ts index d9951cc49..17ff5888e 100644 --- a/src/addon/mod/forum/pages/new-discussion/new-discussion.ts +++ b/src/addon/mod/forum/pages/new-discussion/new-discussion.ts @@ -473,6 +473,8 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { if (discussionIds) { // Data sent to server, delete stored files (if any). this.forumHelper.deleteNewDiscussionStoredFiles(this.forumId, discTimecreated); + + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'forum' }); } if (discussionIds && discussionIds.length < groupIds.length) { diff --git a/src/addon/mod/glossary/pages/edit/edit.ts b/src/addon/mod/glossary/pages/edit/edit.ts index 5bc201f8c..9bc2ee645 100644 --- a/src/addon/mod/glossary/pages/edit/edit.ts +++ b/src/addon/mod/glossary/pages/edit/edit.ts @@ -246,6 +246,7 @@ export class AddonModGlossaryEditPage implements OnInit { if (entryId) { // Data sent to server, delete stored files (if any). this.glossaryHelper.deleteStoredFiles(this.glossary.id, this.entry.concept, timecreated); + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'glossary' }); } const data = { diff --git a/src/addon/mod/lesson/pages/player/player.ts b/src/addon/mod/lesson/pages/player/player.ts index d8fa3b011..2b2c1753b 100644 --- a/src/addon/mod/lesson/pages/player/player.ts +++ b/src/addon/mod/lesson/pages/player/player.ts @@ -18,7 +18,6 @@ import { IonicPage, NavParams, Content, PopoverController, ModalController, Moda import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; -import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; @@ -80,7 +79,7 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { protected loadingMenu: boolean; // Whether the lesson menu is being loaded. protected lessonPages: any[]; // Lesson pages (for the lesson menu). - constructor(protected navParams: NavParams, logger: CoreLoggerProvider, protected translate: TranslateService, + constructor(protected navParams: NavParams, protected translate: TranslateService, protected eventsProvider: CoreEventsProvider, protected sitesProvider: CoreSitesProvider, protected syncProvider: CoreSyncProvider, protected domUtils: CoreDomUtilsProvider, popoverCtrl: PopoverController, protected timeUtils: CoreTimeUtilsProvider, protected lessonProvider: AddonModLessonProvider, @@ -369,6 +368,8 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { this.messages = this.messages.concat(data.messages); this.processData = undefined; + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'lesson' }); + // Format activity link if present. if (this.eolData && this.eolData.activitylink) { this.eolData.activitylink.value = this.lessonHelper.formatActivityLink(this.eolData.activitylink.value); diff --git a/src/addon/mod/quiz/pages/player/player.ts b/src/addon/mod/quiz/pages/player/player.ts index 908918fbf..653a9b89a 100644 --- a/src/addon/mod/quiz/pages/player/player.ts +++ b/src/addon/mod/quiz/pages/player/player.ts @@ -376,6 +376,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { synced: !this.offline }, this.sitesProvider.getCurrentSiteId()); + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'quiz' }); + // Leave the player. this.forceLeave = true; this.navCtrl.pop(); diff --git a/src/addon/mod/scorm/pages/player/player.ts b/src/addon/mod/scorm/pages/player/player.ts index a7b507809..2b689387d 100644 --- a/src/addon/mod/scorm/pages/player/player.ts +++ b/src/addon/mod/scorm/pages/player/player.ts @@ -310,6 +310,8 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { * Page will leave. */ ionViewWillUnload(): void { + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'scorm' }); + // Empty src when leaving the state so unload event is triggered in the iframe. this.src = ''; } diff --git a/src/addon/mod/survey/components/index/index.ts b/src/addon/mod/survey/components/index/index.ts index 633957845..b7e71e144 100644 --- a/src/addon/mod/survey/components/index/index.ts +++ b/src/addon/mod/survey/components/index/index.ts @@ -15,6 +15,7 @@ import { Component, Optional, Injector } from '@angular/core'; import { Content } from 'ionic-angular'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; +import { CoreEvents, CoreEventsProvider } from '@providers/events'; import { AddonModSurveyProvider, AddonModSurveySurvey } from '../../providers/survey'; import { AddonModSurveyHelperProvider, AddonModSurveyQuestionFormatted } from '../../providers/helper'; import { AddonModSurveyOfflineProvider } from '../../providers/offline'; @@ -38,9 +39,14 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo protected userId: number; protected syncEventName = AddonModSurveySyncProvider.AUTO_SYNCED; - constructor(injector: Injector, private surveyProvider: AddonModSurveyProvider, @Optional() content: Content, - private surveyHelper: AddonModSurveyHelperProvider, private surveyOffline: AddonModSurveyOfflineProvider, - private surveySync: AddonModSurveySyncProvider) { + constructor( + injector: Injector, + protected surveyProvider: AddonModSurveyProvider, + @Optional() content: Content, + protected surveyHelper: AddonModSurveyHelperProvider, + protected surveyOffline: AddonModSurveyOfflineProvider, + protected surveySync: AddonModSurveySyncProvider, + ) { super(injector, content); } @@ -185,6 +191,8 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo } return this.surveyProvider.submitAnswers(this.survey.id, this.survey.name, this.courseId, answers).then((online) => { + CoreEvents.instance.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: this.moduleName }); + if (online && this.isPrefetched()) { // The survey is downloaded, update the data. return this.surveySync.prefetchAfterUpdate(this.module, this.courseId).then(() => { diff --git a/src/addon/mod/wiki/pages/edit/edit.ts b/src/addon/mod/wiki/pages/edit/edit.ts index 01edf08ae..b0bf66db0 100644 --- a/src/addon/mod/wiki/pages/edit/edit.ts +++ b/src/addon/mod/wiki/pages/edit/edit.ts @@ -465,6 +465,8 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy { this.domUtils.triggerFormSubmittedEvent(this.formElement, id > 0, this.sitesProvider.getCurrentSiteId()); if (id > 0) { + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'wiki' }); + // Page was created, get its data and go to the page. this.pageId = id; diff --git a/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts b/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts index 809419793..6d4f6d704 100644 --- a/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts +++ b/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts @@ -388,6 +388,8 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { data['submissionId'] = newSubmissionId; } + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'workshop' }); + const promise = newSubmissionId ? this.workshopProvider.invalidateSubmissionData(this.workshopId, newSubmissionId) : Promise.resolve(); diff --git a/src/providers/events.ts b/src/providers/events.ts index a5e6e3bef..81493e9f6 100644 --- a/src/providers/events.ts +++ b/src/providers/events.ts @@ -67,6 +67,7 @@ export class CoreEventsProvider { static WS_CACHE_INVALIDATED = 'ws_cache_invalidated'; static SITE_STORAGE_DELETED = 'site_storage_deleted'; static FORM_ACTION = 'form_action'; + static ACTIVITY_DATA_SENT = 'activity_data_sent'; protected logger; protected observables: { [s: string]: Subject } = {};