From 0c0eceb0780957180010284330e3b4945bb0719f Mon Sep 17 00:00:00 2001 From: Mark Johnson Date: Tue, 28 May 2019 12:33:30 +0100 Subject: [PATCH 001/241] MOBILE-3054 Courses: Update section download icon Update the section download icon to reflect changes to the download state of modules within the section, either from the download buttons on the course page or activity within the module. --- src/core/course/components/format/format.ts | 8 ++++++++ src/core/course/components/module/module.ts | 2 ++ 2 files changed, 10 insertions(+) diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts index 1c87d2aed72..30feb662203 100644 --- a/src/core/course/components/format/format.ts +++ b/src/core/course/components/format/format.ts @@ -449,6 +449,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.dynamicComponents.forEach((component) => { component.callComponentFunction('ionViewDidEnter'); }); + if (this.downloadEnabled) { + // The download status of a section might have been changed from within a module page. + if (this.selectedSection && this.selectedSection.id !== CoreCourseProvider.ALL_SECTIONS_ID) { + this.courseHelper.calculateSectionStatus(this.selectedSection, this.course.id); + } else { + this.courseHelper.calculateSectionsStatus(this.sections, this.course.id); + } + } } /** diff --git a/src/core/course/components/module/module.ts b/src/core/course/components/module/module.ts index b4d8cba41e8..758784848e9 100644 --- a/src/core/course/components/module/module.ts +++ b/src/core/course/components/module/module.ts @@ -148,6 +148,8 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { // Get download size to ask for confirm if it's high. this.prefetchHandler.getDownloadSize(this.module, this.courseId, true).then((size) => { return this.courseHelper.prefetchModule(this.prefetchHandler, this.module, size, this.courseId, refresh); + }).then(() => { + this.courseHelper.calculateSectionStatus(this.section, this.courseId); }).catch((error) => { // Error, hide spinner. this.spinner = false; From 9aa63eb95b45e05b65573daae720bba218d96e3b Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Tue, 11 Jun 2019 13:27:45 +0200 Subject: [PATCH 002/241] MOBILE-3068 config: Bump version numbers --- config.xml | 2 +- desktop/assets/windows/AppXManifest.xml | 2 +- package.json | 2 +- src/config.json | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config.xml b/config.xml index 186608e98c8..91e4d07cde8 100644 --- a/config.xml +++ b/config.xml @@ -1,5 +1,5 @@ - + Moodle Moodle official app Moodle Mobile team diff --git a/desktop/assets/windows/AppXManifest.xml b/desktop/assets/windows/AppXManifest.xml index 4d53ac2d375..be1e56919f4 100644 --- a/desktop/assets/windows/AppXManifest.xml +++ b/desktop/assets/windows/AppXManifest.xml @@ -6,7 +6,7 @@ + Version="3.7.1.0" /> Moodle Desktop Moodle Pty Ltd. diff --git a/package.json b/package.json index 91b804947a0..2da3aff71d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moodlemobile", - "version": "3.7.0", + "version": "3.7.1", "description": "The official app for Moodle.", "author": { "name": "Moodle Pty Ltd.", diff --git a/src/config.json b/src/config.json index 762c0709c15..97284d65863 100644 --- a/src/config.json +++ b/src/config.json @@ -2,8 +2,8 @@ "app_id": "com.moodle.moodlemobile", "appname": "Moodle Mobile", "desktopappname": "Moodle Desktop", - "versioncode": 3700, - "versionname": "3.7.0", + "versioncode": 3710, + "versionname": "3.7.1-dev", "cache_update_frequency_usually": 420000, "cache_update_frequency_often": 1200000, "cache_update_frequency_sometimes": 3600000, From b92958a637c2206aee83d3204bd99b2720a0dbf1 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Tue, 11 Jun 2019 13:50:15 +0200 Subject: [PATCH 003/241] MOBILE-3068 config: Unlock plugins and libraries --- config.xml | 42 +++++------ package-lock.json | 49 +++++++++---- package.json | 174 +++++++++++++++++++++++----------------------- 3 files changed, 143 insertions(+), 122 deletions(-) diff --git a/config.xml b/config.xml index 91e4d07cde8..f3db2044cca 100644 --- a/config.xml +++ b/config.xml @@ -113,33 +113,33 @@ - - + + - - - - + + + + - - + + - - - - + + + + - - - - - - - - - + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 24f38ee3fcf..2314b53208b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "moodlemobile", - "version": "3.7.0", + "version": "3.7.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4815,7 +4815,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -4833,11 +4834,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4850,15 +4853,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4961,7 +4967,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4971,6 +4978,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4983,17 +4991,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -5010,6 +5021,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -5082,7 +5094,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -5092,6 +5105,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -5167,7 +5181,8 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -5197,6 +5212,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5214,6 +5230,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5252,11 +5269,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.2", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -11248,7 +11267,8 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", @@ -11273,7 +11293,8 @@ "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", diff --git a/package.json b/package.json index 2da3aff71d7..94337dbbf65 100644 --- a/package.json +++ b/package.json @@ -40,101 +40,101 @@ "windows.store": "electron-windows-store --input-directory .\\desktop\\dist\\win-unpacked --output-directory .\\desktop\\store --flatten true -a .\\resources\\desktop -m .\\desktop\\assets\\windows\\AppXManifest.xml --package-version 0.0.0.0 --package-name MoodleDesktop" }, "dependencies": { - "@angular/animations": "5.2.10", - "@angular/common": "5.2.10", - "@angular/compiler": "5.2.10", - "@angular/compiler-cli": "5.2.10", - "@angular/core": "5.2.10", - "@angular/forms": "5.2.10", - "@angular/http": "5.2.10", - "@angular/platform-browser": "5.2.10", - "@angular/platform-browser-dynamic": "5.2.10", - "@ionic-native/badge": "4.17.0", - "@ionic-native/camera": "4.17.0", - "@ionic-native/clipboard": "4.17.0", - "@ionic-native/core": "4.11.0", - "@ionic-native/device": "4.17.0", - "@ionic-native/file": "4.17.0", - "@ionic-native/file-opener": "4.17.0", - "@ionic-native/file-transfer": "4.17.0", - "@ionic-native/globalization": "4.17.0", - "@ionic-native/in-app-browser": "4.17.0", - "@ionic-native/keyboard": "4.17.0", - "@ionic-native/local-notifications": "4.17.0", - "@ionic-native/media-capture": "4.17.0", - "@ionic-native/network": "4.17.0", - "@ionic-native/push": "4.17.0", - "@ionic-native/screen-orientation": "4.17.0", - "@ionic-native/splash-screen": "4.17.0", - "@ionic-native/sqlite": "4.17.0", - "@ionic-native/status-bar": "4.17.0", - "@ionic-native/web-intent": "4.17.0", - "@ionic-native/zip": "4.17.0", - "@ngx-translate/core": "8.0.0", - "@ngx-translate/http-loader": "2.0.1", - "@types/cordova": "0.0.34", - "@types/cordova-plugin-file-transfer": "0.0.3", - "@types/cordova-plugin-globalization": "0.0.3", - "@types/cordova-plugin-network-information": "0.0.3", - "@types/node": "8.10.19", - "@types/promise.prototype.finally": "2.0.2", - "chart.js": "2.7.2", - "com-darryncampbell-cordova-plugin-intent": "1.1.7", + "@angular/animations": "^5.2.10", + "@angular/common": "^5.2.10", + "@angular/compiler": "^5.2.10", + "@angular/compiler-cli": "^5.2.10", + "@angular/core": "^5.2.10", + "@angular/forms": "^5.2.10", + "@angular/http": "^5.2.10", + "@angular/platform-browser": "^5.2.10", + "@angular/platform-browser-dynamic": "^5.2.10", + "@ionic-native/badge": "^4.17.0", + "@ionic-native/camera": "^4.17.0", + "@ionic-native/clipboard": "^4.17.0", + "@ionic-native/core": "^4.11.0", + "@ionic-native/device": "^4.17.0", + "@ionic-native/file": "^4.17.0", + "@ionic-native/file-opener": "^4.17.0", + "@ionic-native/file-transfer": "^4.17.0", + "@ionic-native/globalization": "^4.17.0", + "@ionic-native/in-app-browser": "^4.17.0", + "@ionic-native/keyboard": "^4.17.0", + "@ionic-native/local-notifications": "^4.17.0", + "@ionic-native/media-capture": "^4.17.0", + "@ionic-native/network": "^4.17.0", + "@ionic-native/push": "^4.17.0", + "@ionic-native/screen-orientation": "^4.17.0", + "@ionic-native/splash-screen": "^4.17.0", + "@ionic-native/sqlite": "^4.17.0", + "@ionic-native/status-bar": "^4.17.0", + "@ionic-native/web-intent": "^4.17.0", + "@ionic-native/zip": "^4.17.0", + "@ngx-translate/core": "^8.0.0", + "@ngx-translate/http-loader": "^2.0.1", + "@types/cordova": "^0.0.34", + "@types/cordova-plugin-file-transfer": "^0.0.3", + "@types/cordova-plugin-globalization": "^0.0.3", + "@types/cordova-plugin-network-information": "^0.0.3", + "@types/node": "^8.10.19", + "@types/promise.prototype.finally": "^2.0.2", + "chart.js": "^2.7.2", + "com-darryncampbell-cordova-plugin-intent": "^1.1.7", "cordova-android": "7.1.2", - "cordova-android-support-gradle-release": "3.0.0", - "cordova-clipboard": "1.2.1", + "cordova-android-support-gradle-release": "^3.0.0", + "cordova-clipboard": "^1.2.1", "cordova-ios": "4.5.5", - "cordova-plugin-badge": "0.8.8", - "cordova-plugin-camera": "4.0.3", - "cordova-plugin-customurlscheme": "4.3.0", - "cordova-plugin-device": "2.0.2", - "cordova-plugin-file": "6.0.1", + "cordova-plugin-badge": "^0.8.8", + "cordova-plugin-camera": "^4.0.3", + "cordova-plugin-customurlscheme": "^4.3.0", + "cordova-plugin-device": "^2.0.2", + "cordova-plugin-file": "^6.0.1", "cordova-plugin-file-opener2": "2.0.19", - "cordova-plugin-file-transfer": "1.7.1", - "cordova-plugin-globalization": "1.11.0", - "cordova-plugin-inappbrowser": "3.0.0", - "cordova-plugin-ionic-keyboard": "2.1.3", + "cordova-plugin-file-transfer": "^1.7.1", + "cordova-plugin-globalization": "^1.11.0", + "cordova-plugin-inappbrowser": "^3.0.0", + "cordova-plugin-ionic-keyboard": "^2.1.3", "cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle", - "cordova-plugin-media-capture": "3.0.2", - "cordova-plugin-network-information": "2.0.1", - "cordova-plugin-screen-orientation": "3.0.1", - "cordova-plugin-splashscreen": "5.0.2", - "cordova-plugin-statusbar": "2.4.2", - "cordova-plugin-whitelist": "1.3.3", - "cordova-plugin-zip": "3.1.0", - "cordova-sqlite-storage": "2.6.0", - "cordova-support-google-services": "1.2.1", - "es6-promise-plugin": "4.2.2", - "font-awesome": "4.7.0", + "cordova-plugin-media-capture": "^3.0.2", + "cordova-plugin-network-information": "^2.0.1", + "cordova-plugin-screen-orientation": "^3.0.1", + "cordova-plugin-splashscreen": "^5.0.2", + "cordova-plugin-statusbar": "^2.4.2", + "cordova-plugin-whitelist": "^1.3.3", + "cordova-plugin-zip": "^3.1.0", + "cordova-sqlite-storage": "^2.6.0", + "cordova-support-google-services": "^1.2.1", + "es6-promise-plugin": "^4.2.2", + "font-awesome": "^4.7.0", "ionic-angular": "3.9.3", - "ionicons": "3.0.0", - "jszip": "3.1.5", - "moment": "2.22.2", - "nl.kingsquare.cordova.background-audio": "1.0.1", - "phonegap-plugin-multidex": "1.0.0", + "ionicons": "^3.0.0", + "jszip": "^3.1.5", + "moment": "^2.22.2", + "nl.kingsquare.cordova.background-audio": "^1.0.1", + "phonegap-plugin-multidex": "^1.0.0", "phonegap-plugin-push": "git+https://github.com/moodlemobile/phonegap-plugin-push.git#moodle-v3", - "promise.prototype.finally": "3.1.0", - "rxjs": "5.5.11", - "sw-toolbox": "3.6.0", - "ts-md5": "1.2.4", - "web-animations-js": "2.3.1", - "zone.js": "0.8.26" + "promise.prototype.finally": "^3.1.0", + "rxjs": "^5.5.11", + "sw-toolbox": "^3.6.0", + "ts-md5": "^1.2.4", + "web-animations-js": "^2.3.1", + "zone.js": "^0.8.26" }, "devDependencies": { - "@ionic/app-scripts": "3.2.2", - "electron-builder-lib": "20.23.1", - "electron-rebuild": "1.8.1", - "gulp": "4.0.0", - "gulp-clip-empty-files": "0.1.2", - "gulp-concat": "2.6.1", - "gulp-flatten": "0.4.0", - "gulp-rename": "1.3.0", - "gulp-slash": "1.1.3", - "gulp-util": "3.0.8", - "node-loader": "0.6.0", - "through": "2.3.8", - "typescript": "2.6.2", - "webpack-merge": "4.1.2" + "@ionic/app-scripts": "^3.2.2", + "electron-builder-lib": "^20.23.1", + "electron-rebuild": "^1.8.1", + "gulp": "^4.0.0", + "gulp-clip-empty-files": "^0.1.2", + "gulp-concat": "^2.6.1", + "gulp-flatten": "^0.4.0", + "gulp-rename": "^1.3.0", + "gulp-slash": "^1.1.3", + "gulp-util": "^3.0.8", + "node-loader": "^0.6.0", + "through": "^2.3.8", + "typescript": "^2.6.2", + "webpack-merge": "^4.1.2" }, "browser": { "electron": false From b09024b7a85f4b70993b899760d7278d279aa29a Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Tue, 11 Jun 2019 14:21:59 +0200 Subject: [PATCH 004/241] MOBILE-3068 config: Fix npm audit warning --- package-lock.json | 1068 ++++++++++++++++++++++++++++++++++----------- package.json | 2 +- 2 files changed, 825 insertions(+), 245 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2314b53208b..7288e59262d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -498,7 +498,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -519,12 +520,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -539,17 +542,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -666,7 +672,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -678,6 +685,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -692,6 +700,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -699,12 +708,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -723,6 +734,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -803,7 +815,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -815,6 +828,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -936,6 +950,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -955,6 +970,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -998,7 +1014,8 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", @@ -1589,23 +1606,15 @@ } }, "async-done": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.1.tgz", - "integrity": "sha512-R1BaUeJ4PMoLNJuk+0tLJgjmEqVsdN118+Z8O+alhnQDQgy0kmD5Mqi0DNEmMx2LM0Ed5yekKu+ZXYvIHceicg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", + "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.2", - "process-nextick-args": "^1.0.7", + "process-nextick-args": "^2.0.0", "stream-exhaust": "^1.0.1" - }, - "dependencies": { - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", - "dev": true - } } }, "async-each": { @@ -3627,9 +3636,9 @@ } }, "duplexify": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz", - "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", "dev": true, "requires": { "end-of-stream": "^1.0.0", @@ -4241,9 +4250,9 @@ } }, "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, "extend-shallow": { @@ -4356,13 +4365,13 @@ } }, "findup-sync": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", "dev": true, "requires": { "detect-file": "^1.0.0", - "is-glob": "^3.1.0", + "is-glob": "^4.0.0", "micromatch": "^3.0.4", "resolve-dir": "^1.0.1" }, @@ -4595,12 +4604,12 @@ "dev": true }, "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", "dev": true, "requires": { - "is-extglob": "^2.1.0" + "is-extglob": "^2.1.1" } }, "is-number": { @@ -4659,9 +4668,9 @@ } }, "fined": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.1.0.tgz", - "integrity": "sha1-s33IRLdqL15wgeiE98CuNE8VNHY=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", "dev": true, "requires": { "expand-tilde": "^2.0.2", @@ -4672,19 +4681,19 @@ } }, "flagged-respawn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.0.tgz", - "integrity": "sha1-Tnmumy6zi/hrO7Vr8+ClaqX8q9c=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", "dev": true }, "flush-write-stream": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", - "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", "dev": true, "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.4" + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" } }, "font-awesome": { @@ -4815,8 +4824,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -4840,7 +4848,6 @@ "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4853,8 +4860,7 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", @@ -4863,8 +4869,7 @@ }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -4967,8 +4972,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -4978,7 +4982,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4991,20 +4994,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.2.4", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -5021,7 +5021,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5094,8 +5093,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -5105,7 +5103,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5181,8 +5178,7 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -5212,7 +5208,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5230,7 +5225,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5269,13 +5263,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.0.2", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -5439,13 +5431,15 @@ } }, "glob-watcher": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.1.tgz", - "integrity": "sha512-fK92r2COMC199WCyGUblrZKhjra3cyVMDiypDdqg1vsSDmexnbYivK1kNR4QItiNXLKmGlqan469ks67RtNa2g==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.3.tgz", + "integrity": "sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg==", "dev": true, "requires": { + "anymatch": "^2.0.0", "async-done": "^1.2.0", "chokidar": "^2.0.0", + "is-negated-glob": "^1.0.0", "just-debounce": "^1.0.0", "object.defaults": "^1.1.0" }, @@ -5502,24 +5496,31 @@ } }, "chokidar": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", - "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", + "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", "dev": true, "requires": { "anymatch": "^2.0.0", - "async-each": "^1.0.0", - "braces": "^2.3.0", - "fsevents": "^1.2.2", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", "glob-parent": "^3.1.0", - "inherits": "^2.0.1", + "inherits": "^2.0.3", "is-binary-path": "^1.0.0", "is-glob": "^4.0.0", - "lodash.debounce": "^4.0.8", - "normalize-path": "^2.1.1", + "normalize-path": "^3.0.0", "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0", - "upath": "^1.0.5" + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + } } }, "expand-brackets": { @@ -5543,133 +5544,676 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "^0.1.0" + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" } }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } + "optional": true }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "sax": { + "version": "1.2.4", + "bundled": true, "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } + "optional": true }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "semver": { + "version": "5.7.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, "dev": true, + "optional": true, "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" } }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "string_decoder": { + "version": "1.1.1", + "bundled": true, "dev": true, + "optional": true, "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "safe-buffer": "~5.1.0" } }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "strip-ansi": { + "version": "3.0.1", + "bundled": true, "dev": true, "requires": { - "is-descriptor": "^1.0.0" + "ansi-regex": "^2.0.0" } }, - "extend-shallow": { + "strip-json-comments": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, "dev": true, + "optional": true, "requires": { - "is-extendable": "^0.1.0" + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, "dev": true, + "optional": true, "requires": { - "is-extendable": "^0.1.0" + "string-width": "^1.0.2 || 2" } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true } } }, @@ -5730,9 +6274,9 @@ "dev": true }, "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", "dev": true, "requires": { "is-extglob": "^2.1.1" @@ -5790,6 +6334,30 @@ "snapdragon": "^0.8.1", "to-regex": "^3.0.2" } + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true, + "optional": true + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "upath": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", + "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", + "dev": true } } }, @@ -5843,21 +6411,21 @@ "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" }, "gulp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.0.tgz", - "integrity": "sha1-lXZsYB2t5Kd+0+eyttwDiBtZY2Y=", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", + "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", "dev": true, "requires": { - "glob-watcher": "^5.0.0", - "gulp-cli": "^2.0.0", - "undertaker": "^1.0.0", + "glob-watcher": "^5.0.3", + "gulp-cli": "^2.2.0", + "undertaker": "^1.2.1", "vinyl-fs": "^3.0.0" }, "dependencies": { "gulp-cli": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.0.1.tgz", - "integrity": "sha512-RxujJJdN8/O6IW2nPugl7YazhmrIEjmiVfPKrWt68r71UCaLKS71Hp0gpKT+F6qOUFtr7KqtifDKaAJPRVvMYQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.0.tgz", + "integrity": "sha512-rGs3bVYHdyJpLqR0TUBnlcZ1O5O++Zs4bA0ajm+zr3WFCfiSLjGwoCBqFs18wzN+ZxahT9DkOK5nDf26iDsWjA==", "dev": true, "requires": { "ansi-colors": "^1.0.1", @@ -5870,7 +6438,7 @@ "gulplog": "^1.0.0", "interpret": "^1.1.0", "isobject": "^3.0.1", - "liftoff": "^2.5.0", + "liftoff": "^3.1.0", "matchdep": "^2.0.0", "mute-stdout": "^1.0.0", "pretty-hrtime": "^1.0.0", @@ -6251,9 +6819,9 @@ } }, "homedir-polyfill": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", - "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", "dev": true, "requires": { "parse-passwd": "^1.0.0" @@ -6744,14 +7312,11 @@ "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", "dev": true }, - "json-stable-stringify": { + "json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "requires": { - "jsonify": "~0.0.0" - } + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true }, "json-stringify-safe": { "version": "5.0.1", @@ -6777,12 +7342,6 @@ "graceful-fs": "^4.1.6" } }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -6912,13 +7471,13 @@ } }, "liftoff": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz", - "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", + "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", "dev": true, "requires": { "extend": "^3.0.0", - "findup-sync": "^2.0.0", + "findup-sync": "^3.0.0", "fined": "^1.0.1", "flagged-respawn": "^1.0.0", "is-plain-object": "^2.0.4", @@ -7053,12 +7612,6 @@ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true - }, "lodash.escape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", @@ -7422,6 +7975,18 @@ } } }, + "findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", @@ -7451,6 +8016,21 @@ "kind-of": "^6.0.2" } }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -8105,9 +8685,9 @@ "dev": true }, "now-and-later": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.0.tgz", - "integrity": "sha1-vGHLtFbXnLMiB85HygUTb/Ln1u4=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", + "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", "dev": true, "requires": { "once": "^1.3.2" @@ -10276,9 +10856,9 @@ } }, "through2-filter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", - "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", "dev": true, "requires": { "through2": "~2.0.0", @@ -10642,9 +11222,9 @@ "dev": true }, "undertaker": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.2.0.tgz", - "integrity": "sha1-M52kZGJS0ILcN45wgGcpl1DhG0k=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.2.1.tgz", + "integrity": "sha512-71WxIzDkgYk9ZS+spIB8iZXchFhAdEo2YU8xYqBYJ39DIUIqziK78ftm26eecoIY49X0J2MLhG4hr18Yp6/CMA==", "dev": true, "requires": { "arr-flatten": "^1.0.1", @@ -10700,13 +11280,13 @@ } }, "unique-stream": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", - "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", "dev": true, "requires": { - "json-stable-stringify": "^1.0.0", - "through2-filter": "^2.0.0" + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" } }, "universalify": { @@ -10864,9 +11444,9 @@ "dev": true }, "v8flags": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.1.tgz", - "integrity": "sha512-iw/1ViSEaff8NJ3HLyEjawk/8hjJib3E7pvG4pddVXfUg1983s3VGsiClDjhK64MQVDGqc1Q8r18S4VKQZS9EQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", + "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", "dev": true, "requires": { "homedir-polyfill": "^1.0.1" diff --git a/package.json b/package.json index 94337dbbf65..7ad1f91ae4d 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "@ionic/app-scripts": "^3.2.2", "electron-builder-lib": "^20.23.1", "electron-rebuild": "^1.8.1", - "gulp": "^4.0.0", + "gulp": "^4.0.2", "gulp-clip-empty-files": "^0.1.2", "gulp-concat": "^2.6.1", "gulp-flatten": "^0.4.0", From b66e87b86b26786f32fd795fcfb97f9b3b8f8d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 14 Jun 2019 11:03:50 +0200 Subject: [PATCH 005/241] MOBILE-3076 splash: Use full size splash screen --- src/assets/img/splash.png | Bin 0 -> 55907 bytes src/assets/img/splash_logo.png | Bin 16563 -> 0 bytes src/core/login/pages/init/init.html | 5 +--- src/core/login/pages/init/init.scss | 38 +++++++++++++--------------- src/theme/variables.scss | 3 --- 5 files changed, 18 insertions(+), 28 deletions(-) create mode 100644 src/assets/img/splash.png delete mode 100644 src/assets/img/splash_logo.png diff --git a/src/assets/img/splash.png b/src/assets/img/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..e7889ccf91e612b8a62d5ff911239c96d31360fd GIT binary patch literal 55907 zcmeEv_g52L)GpW%K@m^^l_F9Eq)TrqARUz6Q9xRNP!oDoEFcQfd#_TZgccyE^xjJX zM0yR8UP8zn#P|DtxqrZ2>pE+Za~v`==j`+B_MAN!{8Urv@}--XNJvO7t0*gIlaO30 zJo&kJ1~|k2X;hJfpm#3_EFWPpyG(#C;r8nMBzy44F{>G?%{>_1PX;xHsN0({Y~dIm18NA1 zS(5%i_m3Z+CIY4*hnrTIcVX+$9{iI3B4P>WdN6YR&dA=|!{o53SrynCqE$W>8r4vI z)Eziv6_#3})gWaHci&z0Fy~ImpZ3q=SMv5zzg@jK|GC5I$l!2y0vg;x&8caTCQ)sx zEc3F?Ut5L8Thd+*s~*W4Q&--oo)NsL&Hhjck$1N(+P!R55(_QNwsh?1ZDujf*+l+2BX7d8K76ExfeceA+2k((tT170j$nEdrs-{zK?mI12rNf$^KEl?3?8vmk)kMzUxL4_J zF@v&u5h9WgNi@qvzrMOVE)`b$q1s;Ki)UoNSqNDcm5$$2v5y=(L+HVu+x+lpPFhNB zG>6Hdy#Q@fy48;hy2Y|ZT2A-ItzWj>7iqK0H#pxYrPD3(EDZW%-1~BgjEnzq%p&pmy!Ngvca_<&EFZ30?W!=bw7nR)_ktyFA9M zItyB!g||AjKw3U2&3n+>FQEi}iT~!-EGifofvW!Oysk@eXyTcbSyS}ZTl81qt%-DX zYbMJ2D7r9I-@31Q1?R-4sdbs+{uMU~r>ti_16)E}woa0B7ahFG-l^oK_S2ct8%3O% zvw5ju=bEHRVmjqK8^!L&vp*qGB;v^Wf$H^(99N?&jU*)tsKF$sc~Q>E%2!Q(3~sFq zW2-SS>}G>wj;1D3eV)P$@;4|Ec?5&!L!y2|Q{8!&-aO=Kqb*=H!K<5SCXp>p$IaKX zUjDpQUNkU*`YGNy@3i4FD?a0U73BBhsn*+1^e;n)RPx*f-AsjAdg5_;?3N>|dG6(hIXOBbZb1+iV={b( z03ykwT<+GNPa46wo7>poNs(AvvgJgRK=lLnpGRtkzT~a zJtvcpVg?$4;1ynC7kB$y&e(ZBm7wMmyg&LH?UI!3eg9=uvsbZL@y<%JiXX*#bxG>$ zx8jNAzoz{!eyUNGZGtTAhpE(mGP{s3(EUbzEQTEP9IhO3)vo?(`{E58V(y2eQnahP zXy)S%)=-kK_iuSIZx?Ys{$_EF>}J}`4aOhjk`AdQ0_UI4*?Jl-HNI%Nlw1o^K1*n{ zC0~Z~LDQH>i069B-EOb?Frl3APGK2lWx=(I0W`mzZ`(QfTJ(ejY11 z?H7jepr{_Q?|``;HySk?dg_0<7fw!UytR-zgzy3 zCCNV!k^Ivhl7D7{gyf&S{O2eD=lIVzGKl7FEAU=aUh6#qhle>vny z+Wapx_!k-gF!;wB{0j~Kg$5@E@&6JU81Al5W_`a$(zwL@Su*%m&{#-xG`u>*T|AH? zNI2;17{!ezXJjwZm(be9&vZ)cyu;s@t0uX6=YOe6>XRFv9120FEkKq&{$Hw+^}mxr zSO1s&y!hWqR)zn1|85Cn^Z#V|4@Cd82QUfL|IEfedkJLo{~X0X5BV?JILYS!g$Dl* zga({54pRTs0-OY{|CIBea{g1!6HWMkKso;@ON9Bmhu-1g=sZUNt}jQo>grP+Pj z*pR%jcPq|e*HcC4`8wv<2@wA$8n*w))RWZ^Q2p{hge&mR#(R%wPEP-KWS&#`uY1z|yBHAI z+lwaxe07v%>)OLk6Wq`s3&K{|4_^F>a2NAa0k=`)C$w7+EPHTAOYRUG8X9_fdMYa` zcXxN+zkeUesl^f5K3rfRx78(l{``3b%2P4#vqb#qoNJYU+PK3+6B+60m#|pu^71n9 zxwf{pvLd+?GBPsK($ez%`*$5!=N>@^nO9g?n4d4ya`o?G{i;*`L$V8)YC5fJ5fI16 z$Ls6sp`oF`V^hRkGfGPZqg8>STMNajbC8jd_1@oweOdG#HFfWf)ROha!bhKEKC`4a z9a0kA>0M>H^QVAHsG$fl@$jt7`L`24gZaqnDCov3igoXwZ*O>6%-W}sbu$m zWroR0TD4I2Ze0{Vy^+hEQ^8a_<|k<8YGHqcwaz9dC-d_1e*XL!4u>-`GUhhKsdM1* zcz$R-F>s+PerwDsY|^!rk)56W_J1(-6F(J^Uz{-FXmHs>OG`^hNy*pO*TlpG=mQ-c z9UU_>GcdTs6qtC(M+Mdb{i4GkbPeA;mIsvC9$TCahlEb?57b||zbqfu<-Q))N*k$YwY8fBpLPh5sNmv#@Y&`UCUvcCMU^_og+7H~H^QhUuk0 zBYIWZBe9sY<&VtGU7Tx=)X>lt1W6tvBu)UcK^Rnd|f>zsgS4`4f3=B|dj>89*C=l-kx*OC(8$@pS31PX=n@%2^eKoPl4 z&@FfL`vl#dE4rblSCWvtzw}2-8ZV~h_taPmc?0?Y2wVQc73_F(HN%=Z+WhX_sKs1ulAf?El_bP@y&1?{;O_&Y3T+al*uYErqT&ri$Eype`w#ezGb!a^7Pv0ZKu+nqt;`*eH*YY(ti8D4|i6n7$t|Mrexjr2&qmRGtH^^iACqy z<(wZs9!uUly^@~eRM0iK`3*QI1hS5<0{5oK0H0t-$J@*J6MKFGt9q$?kiKbo}InDRB_iCY!zmz+SLj;EG#f~wT&Ge9ZJmp+uzBU3-j`R z{m0`zul_sOSNS$V@qi^;pF}|S)+fRBJNgC&qxTR`dbeWk8*Oe6YFR)hJpoX9NS;3O zD#@vSK9pDlRxylGs?vTi#|d32A|}R36WkAt2iA0N$`@x9CZn&XH}B0v_-o3~1+tyo zfrVwbH>K{l(bB8Jkv`Z(gTbf@dq7>5J$&w)?sP=+ zz!sqaw(Sw@(=Ks>e;+k>>R}rDl(V$~yA4Rg40se^Yypf)jZUG_x58TWqF?;??Hi|( zP|C<(_Bb3XwE~uE1b`P{Ui~r5phx16wXxBLT{yqrG{p&4s$FO4ub*D~u=v!Dnbvqo zn;RJHb?}rpPE?m>XTPN6Bc}HE_5J<~_RB0U-%9_$lpyZPYV~(sZRDrZ>V!D#!8A0w zb2Jjoz2~DB7Hlk8Spjvz5Z0k4#>Odro9zt3Hin?!KlTpDdTd*~t$c z`<4!ed)~(#6Vp5-2|p)u*><0Z3m z^~h@3`g7Z5Rd!YLEriY{ti-Ins(zctfBfpLYO~kOb(Op^g-$k8wtgC}dm^R4hafwx z#WpN+L{rid*KG6M!E4vOBxw{o0a3UoHIm);kpt1+bu@I`yuy^I^FZ5Eh zl-~HruI=!nnT9LIE|}|Hv|;JBnD_zJlN)g;tJIJyHPM$cg*VjshUf1aX|S^;M|r3Y zjXLTB(wgo-`SWXQ4?z}qltII0bfR-D*Nu?N2EqaQk*+E& zp5F|7QSbs4$rU)hx(Pk3^-(QE?PF(9j-r46+A>k0>8&P_lU0;;CirrQ!97t+M}>@+ z+OnOxU83`YUHhs!Pw)4iq2-BEb}Z!YNj6UVxM!~vSvlmeMT#ws_9)pWL@d*cdWIuY z0&uss3;7Fz;_}9{$qkh4l7d5HRXW$A3c8{savU?R-AFXNGfux4t^%sFxZrDMl397X zKc^mrVHpo--&LuUHzhXvY;|2;uU{7@P|#inzj@oiW@>3k+Z}b?X63Q4cO@5J><>#? z-sjPWraf~0{Hm08T8R+2GPE2rtOAQXty+K`iri4E7w4Vy;y7*<5tHdsh~>aItz_pW(Qnw}bo2!xE5e{^wzo zai)_mTLVlv!(X3=YXjW`rln@Qx@@}0{OKc?&}>O;2YZfNLvll-W;}0cMyWZ@IN7=! zg_ex`l$Ybn)$5-P?*eZtsIz zTDBcB+-Gbnss`ah_N#(eaXvF8xNV8G2|oyu7qlHzQAbSCX=qcAv*PFx(pgC8SmvOi zT@CjYyy=|VGphR-eG^`zj}8MZ3S!6Zw%?V9D0!OM4isr#=9LMMENYE1Q-a(_Xm{IeUZ6dzPo+v7F$1=psE5C^&UWI3^ zZrBEOM#tFsQiu|kRaG>TFo`n8U2z_H&zoTP#bZ|B!cpMHXcVM4vd7a{XN^@@eZ#bT z$yo|#tRp*q?M7J5%E)rou4he-u|d7L2}CttalrFCyck%BiX923>tFd|$0&sZg6mhV z2!rAoEbK3}d`{o7*VAZ$=bC8^Ro^w)o^w`#Q+C%cCHZRB2N`hnbLDsy$3sJs-Qg!d zjV=A`tI(0o@pd$KJfDyT|E1FovsQ!$npaV`x}UQgDxi_DIkfh-Hg!aMkKv02b0<#CSe;y- zurv7)Dm5{)>%Y;)ReEUA6jWNOTCqjMa6;iosKLZ8gAFPf(!(u9ys0B=RM&UUA^@VP zLoVYwHtaY&Yf|C6l^sRKybwFYzQOkyx09F}{iZo*MPCXpDFx2vN)||1eUOCuIXSr= z+Aw=_*WqUi%erDfViWp7g8@1KJC*W0XTpA=xRkPwdoR(KQV=Mk(h(<*oD${p>q+0Mzs&CORJS5Y#^I5iKED=?E{AN-iC21F0V}+lT;poX}Y+__AUZDBR2iX%>J;)@{D9eiYqpXHXzj+sbHI zV20mvJ^mQ9I=Vgz$9qbMqoesmI+-?anA)~z-F&BW^2RW|@2O)^P)Kqo5C}lJVhBW; zK=^KW%xnzE!B!3*pBFIwHJ3v@&Se9cmLE)NP~VO9RY^3bEVkK=_*C1qK4wLQ&(O_5efW+;}8b&bUYzX#9Yizy>y}ZRv zb4z6JE_T(8tg1G!1T2U>x;SA>OD0g-CfMo9%yt?wHwhjDaG)Ll9E!_z<{_Ji<~I6Qqt0*B4xM&M2@y{ z#F{*OH|D5&gI2bJ=~b#{ck5AAKt^kT#2Nj}-ZoLtB=o2yc0x*Qd{qv#$R{@2@kF8C z8SwhSKaVS6gE^yTWNZR1n)|5E_)au9KQ^Npq|&L) zfBttTHgrnEd65bF+Z!>bGc^oN_{ep4|J5tq0r>4L=#gp|L6W)3m1^C?cNcLNS;>>~ zTYt*F#lTJSA(QlC1PZl;^Y3Q#;p5kS!Uwwq>b+I#{-L0ugU@Ho+`V@>(ixa8$)|^Z zvZv?jYPo|-VN5^_jqT(|P7Dt#K722QT1_P`n`O5T;F+p)Y*L{mbsV$K7>+8eFN$y& zRM+NME=nANxz;BVg6h}B>Lk?IlUhBa356iZ6-dJ`hXA1;F+_}*oi+_-b(hi26Fl}Z z%*O!pqJ#7EH&x19YwdL}IB~b!=V+X|@(3n6%1du0h#`V34P=+esWD5cX!-Ujk46!%<$654balJ9V z)?`-&w<4uJ-bC%KbRL*!i|8LE7kC0tEC6XGHsv+@j?O%V~_qZ7DOw`)+89s2Um6tN# z6kGH7fZK5P8|X0?(!K)f`)2{_dY=EQaz4;EEovQCRd56&xYivqJ1c=N~XzY=Vf7@eFX zjF^Dyw+GY!;^D=CoPe5|@Lum)@OTuYZtlaBkokYL03}Wl>m{hwb&2xgv`MGqRSc92 zDdBVD*^KXGs648oxA>q=4EB;K`fIO^Dd7&R6S=aH8un^yfgkJxwO{2wvcL_^YWWt( z*>mT7es#HUISXd@0eyJa$=~&XwN+9#c(8@}B@EtMy#EGDAmlh&n#UGG8$iWdRmY~F z0D(bc+1 zHtX{oJ*~Ct1~wesqTzXvDq9OeDAVCco{7>%HVnxmmsUKct1j&Qjlr1AXR^oh_!f+- zs5gY;$)cTmJ*v_qj5+d0I}pdVqpgb@zgiYUoOP`N(S;W!cwh zE^WnJ0A}6>JJsbv<=nyH;bA%ac<=FY>haf{9Dv}x8ri5&?Y2t+=OnqVJK1mfvjYS++hA?ldN9lc}E=qKHJ3fVS7ORj+LBSEmX>EqQWFJ)Q-L} zVfdHN=m`E)u=K7Xn>;Aq_(tpy+v%%fLrbF&cklT7noZs*eQ7h}LHluT5Tt`$-NYQl z`>vL}bXmNp)%^Qqnz7b{#4-sRzidM_n#SL6C01AR2?ogQVN8_A&oQr0cp<|~C8&K) zpRWG=EaOQ4*ob$%4KySzAM1Vhj+-rx6s@f8?hwz~C^*N*1=#6BAGIapZHqgxM+qYh z#WKE6Ozvz`ncMHk*?}V`w)CW4LavC)XRMlu;hElpS{pM1OV2Q{EBdMG9iA*Bd*S-50^HBk;_!i>8UAdbgN>+A@ zwQFjD>Au7QGndO$Y3Wp8%SPIzIX0ukW zl($VN9d)w%XA!^xjL^z4I(?=0Pb@DvQoOf~QGa<|ig&ty4C&+sBzN0b_$+4w{>6VmIJE{KstV!xKIurW z9%hNfo?>>F9EH)oHl35yIaf4*6kD?V6g15ko%EoPJJCtznPeI#yGuArdM1 zs8|?!*S*G_2#1EIZnCtyF^gqL=biPVyWnZh8ks)80|2Zv(&AkHKEcVm@o1>dJ!KCaEHpRa0uc4VWRQ|w@3wF`%hSjjPnDBdkT3Pm01 z5VQGewy4&QqHF3iPeO$1tEZkspWcc!ye;0sR*0c@OI20%5(ULs-^&15URdb3)>PLv z$lr6zRHml)=F4*_>B*}{v3ECr!AgY}sPLLgP0t~n?#``ZYb4~x5d@@9?T8b6pfUG{nf4C)!y|rD>^Y&c zbak(VGzYZ^kl*sI;C5Bg(`s^8n7xfUeuFITlir5Ksr)o7G$F$ui4nW8$7JmfuDVl) ze@{MMCCVQWXNu}%_N7U%4&4pAW<>|PhP@=>v|mDF%T{Mnh)Po#t7m0$9u12y?OGh5 z`;$poSTnem(Ot^;HXOYlbnO*!)531_f)L46E-VBo%hTf3DXYnJf}Ahl`*(n*k$x`e z7CrsF`}gnPyLa)Bx0;a_{6psMo|Z;{n={l^ywgjH-^rT~ag0ZaIlj!EJ$t;6iZHX_ zJp0Q(V)*z_ZVQU!TeeOE^1>K@g0IgK@he&k-yd%i+uiXp5UM!@wHv7THerAQ=&4y|5tN&&X-mA+9i1{h^u< z3LWlGfGwxPEU7o7eZ&q*h;XSoskqElIHDW#+h68D`gJRNoxyK{6~3IG5D%$W72vab zjzAuRkL0D^#Q!*v6Cd>-N|QcBS0rBDCx!0Pd7sBIf;K~(q=t8yd9;WldS-(kwcn@j zPMgM}4>XWAxl3PC;=ZW7gcf>A5mP8%QK$~@-#WG+Hr{Brc~5W_?pp=#8_4X34Utr* z0e}R-W>@k*c(M9@(TCu?-0(0jq zNvr_tF#Pu3=<#Pr{UZTB?@PpFkc8}~KZ#7^Jj)+C)hawcNBe7%?wO0SSQdel@L}2k zWPJq)V*lnQNvz;alawuJNo3d4+sVI@q-uL%Tel(#Ilj?}eVc?~aTCF)X=C_i`EiQa z#4hp%^7C^GKh}!lAF$a}xUZMxJaEOR6H#x#rwhhE}~QAHNs zIm$RW({vskZpF6Np;+QxfliQQaNlNLhf z909} z(}$DCxOf%!deHjSFqqp(eCQSoodX&`Rp3Y@vaU}07aIu(yQFr8o^GJkmV+r_P>-oo zprY7AA;%P3;P3-znQPc^LY;sJ~l*; z&`fqRw_iRUL5fA%jC&hcOq+^-a4)urMH7=JN|r6pO41Z7kAiirbQ_#g8MZ5hZpfIy5yNFEiYH*eHbKfiGeziTAO($XdnB>yucBk6K z^Z2iJ>1l98ju>;3`YA+atcProIj2tPT-#+ZU{)zZVaiAJd81smFI}hrP~t?)spdQ5 z%|77ijNSvdSNU>>=c&%5Bw;H|mdDaPb?&W?gm0Hwb^Fa`a)~kz~|&h2}&S z^U}9!8V7ngjb8ztzVa zH$YCweWE1>f0&0x!>s%s32MRb&N{*^Z+}krQ_J5iD|>(+JDR%1vGbuwDDVAq!;$(G z&47U(Lzi(_DR7^|k_hpKlqJ&pGJu}I>|_sGZA_Pyj-IVs*CTh(2k;f?ncB?x^RQ8o zbTm_}^4eWOn#Ta9UQ=qSEtVprOO&=)ESf@NSV-7DhwH0Ac%0!~%abh+T}8bid(li8 zMwC~Z$a-Xq45~gjDdI(}<{{dmFNV&2_sk^&)V!A0=p?vmC41Sk|b!uh9T9ucsSPaqaG&8p$ZqgKE>bGRX+~du>u+gJ7Pdzo1CXbmH102wn z!QG|q>*Z=-v1ot;-47}Ll%U*o?MCO&_*4hl)SzGlxw_ks=5*^GrGVoWrl$7n`oqvC zA-tl4vdfc4QANhv&uA@rZiJ38P;@nw#GMCCBw~ONED*7J=;XY(H`&Er>2T-6pPfsI z)54z?KEMioev;X5cwbapT>NpMje^aM!?2#4b?#4a)ykqUqP?8yOAd^Kjj59@pBQa! z;tyBLB6|Fk!`u8k+QkP`B~G8$_hn^Dm9&au9o>7(%U4Ga86{m(xr(YkA%~vMLj3u| zdZhRb9XJZVM10NhDC0`CY55$htoxX{G-aPbCHFycU&EDU>GZSM0e@2jWKL+owo?>= zl@lnBL2;EASCIJ|l*@x)Df|bQIk**hy545zSVu%@h|K23Wzdi-vfrk*VvCGXH`Wd; zM(EWhdZd}vhT8QO`>9-1*2kM1H>;=EcW{_bYbiGqs#2r(Q*K)*_j^M6e?krzA)D&t zO8mZ$9?+CW+N#cb_zt&DAWSBXEkLU%=>%NnyWPD*FaaEl>wbhz{|adbdyDzE0RAC# z$BmX}^9JTQPtHp*{vmk`6h`gR1g^#LU`SETcC-k>>C|8ky zB0J3`*qB~kq^>rZ4;%r+hePuGpuP1Z+p~5!lXdK3H^D;Gpn}1`S88%Mb?%sj#N>WB zgUx~&WLg8*gy4#QeknvQtM2u6>P3(cA7QdM(y;G zqRbD#n#&x!X-Z{Gbon%yY`skuBxX9}DpR<)_MEFRJ;)Z=;*%HF?JqZjJo!7nU z*5^^(VNS$PC1@pNL`>iQm=Z57?12*wXue;CT_t6_oW><{oKaBIY~s45$lP=y!uPlT zgkcZUE?v50SvnTvI}Q}Y0J}f_`_mu5)zj};3w(ZYz|hcaTluN_z8R6J9W8V;G%mwZ zFji;zyH8Hx4;~{cTizXv5f7==oZ##RDPrkb++4h*b#1z#0+fa{p z&pe|2ZO9j#UfwUp5@y#KD*e4f(C<#WK=9bzBanD8;h zPbBso>5*vFtP*5hRY!tQ<|{OXyHa;!Nn&X+h_7N)c6RbNxsIu*H5ySQa$QHh%CAL_ zcUO$~G%+QUt+B5QzspgDtGR7mudq0-hh#S1`qK3Aj9vF9o*jKT$BcK!3ngT_ckY@k zLI^*4j2&9Etm96i?7vk9R^bjkp`~HXmi=}f=bAgV>VOgnna#GlRTFNVQCvEjsp&40 zlR1{PMASJae?b^`YGUx750o&KU7VI03(u*(UvWt5JVQy`0o%2zNzf-LVWPrd#+Ud4 z$R)>cmBhrXRW~tlZjE92smrN?L4vQAEp;OD11hA7yYB_vy z-@Iz?rIgk8`Iv7bl}}`Tcbf8bJh4eU2=f`0bIk1tcbV%z+`TPGoAXv*F3X?@@?m`o z>Sy6owcwBd!a-nro#|4k5l{Cwoi{kIfF?`aM(AEv`G~=WP3nv@ety}!9y)fwqj7Kz zw&e)@Wg@{pCNKj;KM1gWE6<~)(tq=a1m(b@&RhPn!V*Rm{wGS(HnLQc1AuUHC{;9PI z$v$pYJ*?){1I34BQ0!!^na8v|$6U2Gc)ZnVn3ag7n~=hE4~se%5o$!IKzs%C4@GC0 zCvs#G-9?W(ovWCMWmVRn6)4`@U~1qwei6{0{x%Di#BSK|UAZm|3nM(`JOkFn4VnaM{gkWS@60}9Rhi8Hn$d_W;)-H-A#qSYwP96CA*t% zps!nO=#wRox)_Vk6jdt@ki!OvQMyi7b-ajMwM!R-U2g_n2Bh=19)^DKP4C&0#Pa_A zuB~rgDPpcOO^Ef{RiKP5P73P$==*`F_bpJtjSfa+qJO&Lh04YK>vYB2>qlYok)$pk z;gBBfdcxk3Mb&v-(SuJOh)Sl|5Oz{KSe4W_KC$R6jUnBMmtcYu_;{FxX{GGONWw-m zz%u>j?A{PNO5UEgG?E$oh4bstN4T-NzT&v&ge1FkevIPmc|m(+Dht`QsuQVlbhnRQgXaf(r`4-&u>&V zcf>%FOAlJSUpfj?sq08;SW*Rd8(@=O%Eyqp91cb-b|X>dc*WBB*XEj)MkZmwsf2I1 zXN#J*`)NV$G+-U?R45L%d&5j_n}Nhufue3+?49g}oO&}sqUMPcD(mtGz8yp|4v8>2 z+?&e~&am0lnF@s2*_w;^Q8?yU`z_^Yc~=p|xN7<6lEdBbu$+3v*h9i{lvJG=%?G#N z+99J}P{IoVV2p~EQ^Qq&i7?H+I#s?!+!V`kM^(e?4=eR-|H#m)>=>TsMrgAl;A#sutIPN*7|J z+(D!qVNi|$lv8>P>lrj#mWCLpUR12RUKQUCohuW|#eid{JiL|OTV%KlAr|;_yiFPn zvix}yRQ<-FS%c0Po(QL&M-`m@9f&{_#YlACMFBZe~f7@s{tX}pWYl_z>LucYcU zWb5d9whh3!OaXU)c`AhKFpy~Z?gb3%t4B}sInadq@PPN6@m5@>@qz2~dAk^YwIMTY zD5b}0znI(8=sS|Vhyy6L?)sxtf2K>mYJiN6gFl%x|0HaRV}_9h$+ylpJrF`qZJR(k z90er;n4hb5$X7|*cYrU?q>t3}GfsI2pJv!!{>!izy~CB8_WUt>XJHh@Q2pv&J$8dA z8w%-e?|XnxEUw>0^}>#~V7s=k-9^~(B9+W`-`aS2W~@#@W7Mar0$JK^$bs+=J;?me zpHwz{!{K9ezJiMj*NYFLU*2fKU)nn0#BMP1@%1ne*4>hZSo8combPU zWM_2>EQQvxB2by1H=e@uwR_G*Ub`Bs01$>iE#zpSk^9cDL2sO3W=009PxFUs^dMV4 zh|YR{4l@}hENAnH6tr1@?~QS9kMRr#Cc9hi@>)hkiM~!Sg?Yr5SvNW&pGtBhe0)lS z*xzGD{q#a({C=)d6Nt<$3cWcE?_ruoqeIT--nsZA^`bjZf;OqiNri);1ug@@`N+(5b zZ^sn&c$1%`;?F<*2OGhhw6w-O!fiHEwhL@h^p%oD_e9|L;1xgoAjP8sS0yHBK`UWQ zw|=k8R`R|S{`uwV(`V04>NkOCpc|-O?axru)6)aWCDWg_1g z9N<>C`5JYyMY2Db4}x$H6tkIk&1A5~dv3o3zi!L!GOta{ON?pV{_HI#otE6X($mL6@Cm zIE`WxWchF~Hx~R^dCXzVhVFAO-%aLT$-(4HJJ~m}XHbxWsnQYfVERc^BzQ|?lIHr8X|IT#T=s*x&NUshHIl%cCqM1#ja{0z!4o3 z;o+;pEND+9^AV9$QCbd|Av7s3-0R!*yN;!1#}ZOo`F>*<$4f2VAlYo}2lz2wdWl~M z;VZS>w9Z0nq(01v&A9cR@Y?eL_hGHV8?sF6+?qZe`o6%dKqgfvGM_#G0Ng{8zr=#| zr{E`<FlQ%bY*z*=PO;N1(DX%jQ^4(D}iPJMGm-?@r#M~-OrwIelC(t zTPKp*S&55idQD=xN5vUp)_rdolJ+CV1WWxNNWlT%?@sB;j z;ZP3j*iF-rX>P6&~x$XRvCs1lY3)FBa~|UZ;*OylK(B{P@C?$4}&+ zG$$b(96JggU)NC=mtMQ``FebNX9b~|#8=Rv!#4iM3=8UGQ`aXw)P}apESm(&B9Bgb zdo|PK)os6c!nb6oXQn^v~`rJb@k$Pwnd)6{U3==#XqS(}N2*!7A`MFbVuO)?Y5 zSU;3LZ`VeHJHK^op{ocKD8G0E8rrEvRn=#UsfxQDgOw@m27A&FvnWpWgRDBAy76U- zwGPh7g`c-CxjX{&5qZm6H2Uj)oSBQp6khwDy ze-#;w4I+1uD&!T>SvX%Ia=(<=xevilqaIhR2yf5~GT* zlxa8x$ook45N&(xS6vw{Rvs!YbA#t6>GIXDF7a@2Zxju`C6Yrrr9Dvs5C@=&d2o8;RuQ zXN)<-0J`BiEjeFAI*pF3$Tw=Mj@)xTo?rL9xk>YCTLL5OFS|7+NKlO#sclZzxl@-$ z+M?d=gu-RsvGtg1Tm&$JRd<#-? z)Jxezpl?0S*Ij@!!W6O?r9XB{mcKHom2g0&wy1)Yxn>IegdFcN=MXI+lJ}^`J8@tn z*SeebK!Yzf$|+A`*ym$%(x*o)ac|zd0X9?vRY`iNisNdyfEteTbb~W)bJ%%Mr=gs^}6f6&fJmp^?*7SqB$z6WzB5IS} z8rVE*QwgJe6) ziQ{}vaKCd0XdCJO7_wy#$t6{i{29|%qYOgLvw`TJjG=L^qu0EXb?U_B{enBA$}M zM0no6^CFv+gLEwK+PNxqK67Qu@4Vvi8Ot1PGS%td6wXTbj~_5gUwKI8d)CZzmm7N| zl9R`V%yAZXpqsomO+qU~3JHw2kH$bVb&|hZXeub6|(tpO-J%5 z-mm=$bmrT+qmh$$&;HiMH5x`Kv+ZKhzTK}Plrmv6qcaV8x3yN7?X7p=Ay60&5X|u|{w};6tMG#P3I2Qsca1NceTnT= z3BiL~#Zravt>SQ(SGQ&&yHS&e-}yn%f-K`VtDzg9wK*fp%|m9rwYOY3n+_@ai&vt* z%VNP7*BBggN`Tc@YPt3P2aT}P!SB?I0(qW@vl>AskZEB%-L+iETLEf2!){BkvI1ox z?Q=^qi6dUG*KdhFsUz8Iejiv=>{#R_G5Hqj=OlGw>36(OJy&W{*lexGoA@%vF@yZ6 z5WiT6NV3iwa~)S~G1dAxA6gN2ps*=J8XL+0y)c?0m*%vy=zMp#95r~;^fi?FtD$uV z_ZETr>Lqjy=uINm-R6f?+gJ+cH(d=+)-E2jidl7iVz>Mt5wE3#GSA!yc)|*dR-o|I zQO9NG14U?(@LeQOi8(d(%wM!<=MAWnzVy+^Vp!0*Y95p!`R4X(DmOs6&Upd<6Vk6) ziH13fjmI`U@EMJZ>pA`CjQDMQ!!=y2m#s_e?L=^HcW>{yKBt!}c>A-8MyE-gFy_EX=<%MltObyd_BvxEdkD(CIb zi7z?SqouKwz4|>K;YJbp6;*bUlil_xM7&tt?u7*q*cLBV7Cdreyr=>(@HX zFx=%F5v(lB@=(oWxZTBdpU%bktJFiKD9Y0Ot z?dp~CHsqYGIA^lR>q`2@ghOS&49_8Z4yV(a*!Q2=Pn3#Yr)+!;7#05Tu*WYOfk0GM z#kR5ZEe_IR3lEhO8q?Cs|MJ=fLvjYl!9y=@PBzY3QtDkKelX93@Qp<@(1nkjru zh;}g~_`(A^ZJ7wwIvh`XwYAh4gsHG=KpJO}Uqw9YG<8JD)vjMEoFc9f;&bM!0qt4j zC$AL=d}1aeKfzy1l#U9CEiAjdyc~f@Y-7RfKttrNJ~f$E$?;P)XZCh3;!0+j{uR^W znPxJlPVA_9AT((aAwgPbQbI|jh7v$RC?V~>alY^Vym#Fn_s3nA_XlBRC3$o9dG>Sm ze)c{mzQVK5EUe#*&pb|eqi$?sZY#N>dKWXWDv}lg`#Zx<*Q(|8tDi4i=^|w=WQCv` zPd)yku^w)7hS|xvpOEgqtz}-_F4ur9vcR2NTU!eXUH~KaXfaZGKM&G#Ee>#MGb;T1 z!3p>;0?b;TfV}P_=A=sm{)wo!!ZxmscFxYOl*!4zD`%y!6V^>5hlj#c$p3ISRUcib=>>c=VK1`mMvI78QGybIXpHOPq%Ud0_h@u%jy*QeRu^a_uXZ zq4+jXYyQczKjfbD)t z`rZa#DDv_1S%EBtl&gD%DKa^Hmq`#__lHw!A2>h)odW+$&-JHxcp{&+qKk^6?tp-u zmsP-mc&mK+eHJSe-r2)f87Q-e&h0}$aLDd*TPwQQiI`-rL)i5`>%B3(?BXY zr^;RV719-NgCwnGOwZhK9DKc-%}3(rxX`1_e;YxoFQ?o>e2?GR*|~J&Lf(g_8-m78 zt~A*Cbcn>(PkokOItMm*yHEW*9-w|jbraem;(4*%>L+t3y-la{HyjT{^QULmo=U7- zx(x~)5oEr-!^csb|NGfWH?1Pr+MWi$HoUEn?EMRQT^r$;funp1LPwO8tWBPSJ(yH_ zeJ=gTIkzu#6zJy7gwv2u%q>#+K{F^L&^zSk1($Dn4fpGFNy*gbm$?2#x;@zOU!5;*~4bN2NDb}BEgYw;3Hb3!sUjD+3#~}gB7}#t6lb$Fjd<4MuOD8rod`}EM z9yr0p$_^*?Wu18RYs6r<#I3(B9y@3L@@Dm)38J^_FWaThivwE>rA()iQysc!F2VDG zV|3a?(l}wFI`vD9lUBNweD@jYxWW_76F2_)`S_U&pr0=M>S|ux5P(~V(TqVLbzilr zQXmBKxSZ=LUm@2kX_WL{bn?LofoRKOtPcs;d{<&HZg?ak#(sbP$Vzu#3mlE(Cgy5zkP=LQZdbqN>+Our~ zDL!Z5N1jmXIx^ViR8F-e#a0ipI`ZXYfzU|{R*evHCX`n$zq|g+Tgjq9;fl&eGFkEr zMMfXQuLrVtkme9IzHgwMja}zJoV4C%%lXa~R4P?PnDpSNkhr+DMs3A|byLt|h6{cC zwoJUabx3`%(SYofq)J$+Y?DylMTzGYqltf_(w{k#$)*2jU|-2EmQ__AF+oz{r?4c(|UTttx|{GN`lCPoM2}hGf29 zl^&i*m{EppJwD}dbhLGNDAnR`jW}z~x`w7X&E^5koP4~UZMO#_BvVLP>h8Vj0_B@o zM`}k@6n=~%hO<(R80V6VGZdIpd0=2?D;R78t@ZBW7KjGjUkeZx@!r!s59Xd9)d>^F zojBr>?`as=?|1Nkz6s4UG>h3HmC072)&#g*ItR%-)u?yxx^`o>hCZDgC7=Guy&$yU z`ngw(%OFs;ASXaJfdDs%7lg2YTLG8I_EV{K8Ml1Ah?AAZb0?LA+$Y;Ix91BUlOF^w z1`ZsXEixK#PN*(JH@!3CEqOOeNKE;>GB3arMo9TJ2=q<+G!P3s3Ijrb{i&`aFZmu;SeK37ht^h1C@XiC?rKADg{fVWt%kUS zLS=esDHAuLC5wO$Y4qytmG?NhYwlWJos0;&W5gS!cyCl&_iUCAJ za)fy12P~~0k--d{DM+GM-#1X=`BO@4Kv-dGmSmBBvwrF7&9tS@gJj*Zz_&B`{=Wr6 zg7-g~pIs`H{!oH$N|g+`>w5YR&aC~%qFURe0kE)e*r-(eyQ^}mgIFhG-(OaBL8F?P zNxH-_UiWhb!UXVrSEG=CmuX`4oL4wUR8;VcFX*g_MsD7F4$n8$3b zJZ(JWL}xOvM)UXqyM~l^X*}B24t_Z&oy^$1V65CO?i8)XT(q=sKQ`dohksZ#84VZR zl|2^R)kFVoqP$hsA1~7bm3AcrNB z64OE^@qHzQOCe;($!fE25OpoV^2|?HjfVYsqk*=a9Pg$_>5dZ_rkiDzEgQd2jE0ZF zg>$Nrj)7)p%|vexKtoi%zp$bqvH6=?xq18L#N>mr#|ljyR_+`(mY zpcMC1_XiZ#JB1)^`*XnQ2o@!#5u{WAe{#lY`t;qYB%FGqZlYx^CC9N*Hb+~SYpEXY z{dxe(kL9(tA(R*8&zGl_PKdsJA8b$Sf-ozs_U``skuQL2eWhVt9aqDiBr|tZ*eaw+2qBftX@g9)vxy3 z%_Zn4>+O+y6fWc-O0{bnbhO-rbsRP#t`?SBzjgM_ZeWNOI6wN${ghPz_u?(hf0Pq* z-~P01sC3#e`He0to#gj7@B^3=tN@oh8G!Du0OSsE&OIcOJ!%815YN0H>}#9d@UCfY z_`%2Rvd2y>+@2T3ps#kJ4`xdJRfV|DwLKYFofzfkN{^l>Ulzs&6_LbG@;iG^iiWq{ zcL)uIWuqCs&_BTN{*(RrtByOfF(cpGck@(--&lAoeE8HWb~*m&w!Z2E1KSGb3atd# z{Q_*7d1A(a&&qq?z=4tCyDwfqAV(*DeO`|a@0};-4_4RK{4|mhGqts{{dzcGXCr_?{eZ?=gqyexdijF^I}B^Hoe-vrHov9e^T{$IB2W6Ha1x1Id|Zt4~K zD78);Ew*!QnfB;WL0m{UekA+)Gkx}>Ouu^u^53VKD_@V-0cRjs{`*y~VjF>z3a)#O zwNO%@foj9i>sbPD2TO<0g61BFy_k2Sj&+Jk)*B4N%n1vJy5Ip|P&lPnxd*3fDti

CNLufl*b6as(quMR#({!th>U$wrya zK*EP>DZ98onF2*0hTH9i+u?r%3j(t8;!9wHGK<{?awEDgXDozP%4A*(gGaYpjSAQ# z6-xOUOGQtW@(jA60n*64`SxN?5t=-OT!P&0%xXRuXGf<(&cF)1M7EOQfye z|D_^!E`RE?dz5UIEwUbAtZwu0YETg7vW+Eeb%f&_ zAiSaT_8pRL@#E26Zh?ZzZ4HS1v0VqEd^}~Vx#NRWUv|~l@}Kdl(FjL_js^Tw&r!3o zKoIC}CsxmK&?glEXZw0Dyp@f^&Rk5QVMisX7FpGw?sZITyG*uPRAH0ZtMaVnuFT)s z|1={Q+g3ih;2Kt=i^HF!rr-_E$6C^sL)Dp2XV(*yTV!hMajIA7zuuoM)c0y^?W7snSW-sb_SQEjDV@z zY2LaAK-|O*NMxq!z1KE2cqA4=6@I=!kJp!V-i(tmHW@(HO@!K9jqgToITtLxfE}0G zD6Rc0=qLz}$dHOg0em2Spzz@zC2Wrv^6jq9D34J>NorS5Gpl^q8|0+iXDbNrr&Yo> zHxlCs_dMjY8_N*;^@_SJ`=hJf{X1s>xy1S0GHGM z;drtG+leA-mA0rQ3_C~^v)W$%egQGtUDh}GCw@)Qqv!L+qrZ#4_Vx}maONAEGi0u45Y*OoEVivK4<$H z1!`p_wG>B|+cj`r%PL*Y|-^>OTz)= z33FQkYH4kx{Bi;)B%Jc`%S zJ)ML^%{@a|Z6+%J3Y!oARj~7yjKcW_n+IafD<9Bu;AU;~!i+>716^InZ)69fKM30x zxSL+3)I>Uk`DjxdVU+_{YAR@b55^FRP|Mu;T+9m%=&g#eYC& zd}t_N)g9@#L~e934r3{&`6NJ_3L58=KYwC)wbzuwo({Esa78z)~!^bw)v{9bCq=Q2E#SVdIhC2dsnK z^)$bKoU80l^hBztG*A7bH@uQECdIB&WZXO^5rqntyXAUo(H5LrgxG=O4wf#q9F#Bz zOl}ZTiQlWXHR7p;Kw1T982XV`RhE>%0cYl%5+uy+oBZYk#jt*ZLdmxh*i6jJWt(i( zVYZfF2~3oXT%FvW?mYsFZGG^b8Q-MlwpJfazOH&KHfIJu>5Zub>Zj&Wqu0`d*am)k zrP%A>%7EXh<>IG-GcbmP@2?ha?dumqWN75`m?uoPYR_SDtE3N2ML2~c?oZ=vi#kB zC+Xvloy-DXgzetNe@;uS$pI2HllUxg4w3_qX4$T5<5jR4m)IOo=Cz!lq1DEH2dj*? z*x^(#q>Tjy3|D%`WQC@Kv5AC;HT$2D3GLs*{61do7o6^DtcJzgSVu$v3lD)P-NqbM z)O~m_*7ct5&ePDT&6mo_mINWDUw`vlpGFla+?iP37#99#f2mPdWwPKNBTz_7S=wJu zQ^_8&Grt(YHrt^kNNcCyV8R*pHXbR?%{^Ld^s`NV__SHm=6p+9*%9|m)sjy( zU|dBUH+4sCa~&FWsE&%q`vJe-Uq#D`H(pN6)yzxOy{8+Bq(#BZAdDRs>aJT*J1_S4 zkG_xYfFGXEXz@Rz5Qqrgsw2vY)56^ET3R{{$?Rzp4yJvJyMW3i$k*+nLyam9X@kKO zGm-d%RpjjsuBk%d;aMg|3~9f`1IADCy7>#PyZ{1labjQ@ko_H#0CMy9KNtU{?yuPV zEjljmaYT1#E@cBrJeb=|{;=|j%9x%{%joMT+A|kMhc&vB+eBxJcBV}Y4 z@wk|=)a(;8sQENy4bu^9{nyc3&hR)cUx@Cu=L(*EO+&KiSN@d*JU)7@lwfr^u}CTs zy^{v@iV?k`8P*uM1}-5%9oFqC`~Bv3zaP|dbV7ot1TZhprLVf-qkHG4y;Cj1G_r$O zK3i|ts0qLEsZf<0GC<5bmXjx^%a&dFPP*dhpj^rL-3xy(-pu_eUQ=^Baq4qIFCdm+ zgERJ+F=KGn=Ci#5U(yiBKvrzp0zpfTpNf=EKsdiT2vxR=(64_)6Jem8>}xeES?%2) zmn`Sogv7ZtR?Iz)wbTlE<3_?bg{Uax(}}Pp%ny# z5NTgFS_oz8EWNF|n^nVYW>L>vhD+wdsv#Wvl;RW@k`FqzPn~<`7{cJ_@=xY-<2j8w(WrH$p zs(mOCmZ8NepW~nQ)J>^utwm7j!@2B-fkPg0>$H$9yz|N=GP7!8AB$@Vk$uR&NRW!N zi-d~pdyO7{UJh+u-gG@_GxG&`eGM(>ooO2>k{?O1sQT$Dfx6sUA;rF^ z5N2lIsHE@^Op@oso$;PDE3&n-9-!&|!>DW4tr z=+&f?`Z|4NtfVccf^A+;Yw~ipT3$w^dgd9(a)af>Ta|SvD=G*(tyTO!MhEVFEAabnc4b$i&LigAh%|O<}wUHaWWbXukSah5jM? z92jj#U2#u7F)2R{ZveedX!X3-&<~!ur@Ozl$e-73O~`cE&sF#JAZO9SQn{szl%)XX zqQo|jbRg4A5zX>7*XpuwjXMhG?VE>D)vkEfro-3`9z;o+6*ODkQ4BX=O7N%&T4*g~3R;k)Fgy<|VLx%?IFN-cT)CNATuUMZe1lBRg$+Ltni2q%gB0Kt z&$C&i{pk9$83nHN?oFODR?PL7aYKX4J6x^DYIvyxuzHJ>KY8>j{dI?p*u%BQdC z{kEA3?3B5ayiwI%QyX)|Jw2{$E82CaIGu|Sz4V|~bLs%KD7IeP-EQfb)c9?4$&R>J zJm9LnW(2&`fyR#!6hFZqeuW)Xw_6gCcZ&`^FTAbaMIIai!{y>p=`Sl;gQJ4N{fbJ$ zxa?a-K{3wFBl{_eP2&Y@tDTRT^7bhT%#^t?E-8O#0$`85b(!EuO*?uU`EziWhJJO) z=3T?b_wN*xe2$%^;KddtVJ0zc=dpXHbEo*Ys`{%K=H#6Gsw1Eq*Z)O!W9FhreenyZ z9bv~a_2WA2>u9wKdTtYLJl3W&FcTE8l+hx!9>WwE+tN70 z-ESc_V8K{sKhfiqYX-Z(Yjmk0jPH|dRzWkQdG5u^Cw(B?p4y8Nsb5z-PsR0BvGV7> z;{x_ksmSYa@!MwFuY$PK9gLlb6!ieVY_tsSEja8z-$>cb_85>qUA24crIMq0&Q$zv z2Yuvsqv0>Pin^lMy?Wy-aw2^SG@2LFl?RB9p5O7UvI1g@8{D10htisnrTb+iBSUsy zu_EsqE<>|@+3w^Oq#e?|?_A}rDP;csT>jI8BWaALwTkoOkt)Pz`paHztx)wW^d*Pw zz4!s0^M=9i-P+(bwr?wG__=MfI%6B@n2wd%fP?mc4cQN66sS2f_=9I|}b-RkgV z?P+fkExNa+f_*_H`s?2cVwB3co)pDusqD~82|D5r>9B)^cy|5l-;Uo|*gz;h95;pnrg1e(1V~6g<{4To z+OTks$Wcg?sSyOtqmza5Wc$@4<~48=k#;JY+r5M=2S&Vvh$X&C8rLR`-XP7Be<77U z$5kSF!{^^7w^7xvBhLuK=1P>_MCXW36i;^H({rqj z#?AI@3k`PTV|RE|UHS0N?b z`6tugQ3h}`+Lz6!XCcCSy>8jeTt*Y}RRuxYLnK;F6Bxkp(1QJ*s+iQ^@%mC!JKq&O z4X0}eSxGg!Mr{J4x5Cb?&;fa^!f0bp{HwpumnyatzUQtf;Q!C-8^tUEJc0tpMu;J# z7>Xy-TN6qZg%e*BslMCs#Oof%pKscCe`*23S{#Hup8qlIt*YS|I3w~ByBYnq;$z4w z!IL6lv|?{gErh3~B-d(2s7IB3-~K7e_Pc!Yln_bb$u#vOJ__yboaWEH+TJ)Pwav@O z@*4foggdAZ{1Q+&Kz}d2b)acg#xq}Ffyd@7 z-OX(rq28BihhRd-Xy>W^Pfoe6A?1jyA@%UbfBI3Xq$0doDS>=%eB-p)7vlTPM<7ziOC zIa%a$kEh$?DHT&^+!&jfb+BP;Dx0Ns%WC5!h&f2KkBsx6QF#FoNSUefT>y!guMqUIH2Mvxq)*P32JO7 zzkwbK3wjDJ{$V>lv0rhXf|wd`&8keAY4r{T_JEIhC81bWex%Z8q`a50I&~Z2Oh49L zATY?OXi^MNncAe(c+FBmwjt+&tFCU+XB6u_pNt3X6*GE}*$!b5D#6Z0Ea^#KFmqOCPKzB#$2(<}o#X6nxM;S<-v3W61~ZLI2m zZ>XK=GZ*p%ZJ19ZcZ?<@0%hF%2y@uGP#ZbCC1Iyp1>va!mfM)jboidah~w?VpVaG{ z40E^No(1%EiXGT&02y|t-oOhRs^)6LyxI2HQ!85jiDWP8m)`k)C``c~Mqrn$EX$9O z8*VA{*z-e}?tp`lSAu^AzD%vu4|?(_E_egP1~`ZN2s#BY*s%h$G5ETM>U?<_suy{2 zD#Vox&J-S>p2>oRIn=w~_1kk@#6fc?$@%E}szM}KhBv($v4hFS>-?h7h);usSk*tW z^&5{`Ofoa6_9teQp?1LCmHUrWeQ$rM`9#Zk;zZ7Q1EaNh;eWw6*!4)UwG%dT&p}!~ zYTB_S@vfV_=j2epe1d9~H^+g;;yj`G>?puyG8wQ3)6DK#dTM?hvIqS>w;xb7T;_ zp*Uk7Ts;EWwt8^}__c)MPj(@k1yS*CpbBa z@SnzG*bmK`qhy>y<3)vs4!T0&DhjJb6@UrncU4eVzW@hTWD3y}eTL?PztcPI1n!!7Jr2kc&_GECth_@>Hb-{u-C9lJI3U1c3FnjEa3q|YfLsWAG}&9D?);A*rr%>eZE<5S zgUB)oW8oCr8X^R%YgmJI*X#oQgJ#3$-i=n5pso8;)aJ7DvCWZJ9U5DwciJiK1BJ8U z9i6$V8_*Z|!4X02T&Nuz*OTd^Ps+Z_B_G|Uc!R$vAmFsVy$s$6iFLGO0#sQ;Us6@y zIg)J0KTgc9Aec%mNmzkJ1sO@;dn~G)?ojsS2^GUUPcHRxr5zbd};lLJ7 z+j>7cRiwwV`&dw*44_#V(7rLeMznfocr&zo&FC9El6$o_*9ttHvyle&p$2WUaq5b` zr9Le7q<0F&)_*&`vU}WDeh+G;8)-ASw;r%>$EnxNMDC8Mv;Sy(W?awsV?s-EfqSjS zTgJEZV>|_avWDB{wVBQXU0OeNy5Vzdb1@bRf|&l(3mH(g7a%Fb=Hy6(9e@E$U*eF$ zd+R>EuHjO`=AI2}PWVHOO@>v37XS7Mm;*t{Ll$9su9-duM3b%}BSXBe>3K3>wB&lL zP6GH8CgnB)q)C+<7@N-I*ijxRjp*y@Y{0h-$$0=b1lI2k2Q1tO+t{L1R~5e?p5TX= zOn%_DMJ{K8;a-2PRSsBtCHV^in%9U^kz%i&jafbAvll*E(?mxz2Laq&&(L zip(-H@`^?Tx%b5#&6->Fthp>Q_lcqI|D79L!{@wA=GMt7d!Lt7InLld-ApPn@5Xc{ zseep)Etedjyxl*(u-1@nr29HIqIQ}#8nB$J$U=yS$!z7|#+1&|t-D6YU1cG$+v%Ne zP-2Pz30XLpNPht0fcGY$-`)hgFYW&wB?k+(T`pgB*%-S?BjW-n?gnbg%olhH_o%#O z?Uf!Kt&v++Rh7YR_0KHq_UkK(BtQNyqIUd$0bZcr9?!Xu!QaGtoP8E>trb4G@)YzbEm+Nn48m45ix zsa(6I3XsHVkw)qi7tOHoOC?8~l&SVYa2Ve?uRa@lQNapeT@A_vym*q2v*LZ*c_U;j zl?;q|Zs|&=98Xbw+_S+TNI$A@_j7xf%b9!W@HD&(cIQOAnD2ig6q zl;gebQ1V-`u%)QBP(Lg~-PtEoLv5G4GnX|8^p}M1(3+B_Rf4W$3n}nIGj^dVyM(+X z?XwT1^w#!i{XciQeQ}F!QB!fl;IroD;^#k)`aSwBTS%8r21|3$!exP=xRu>B4J;XMwiDYlVXB+xhMhB#r${HNEsGKK%rVUcH1T zA(Mfx-U-B63u~l1hE!_TN{NQ5CW@XYNiH0r11$lBCs)2<5-*3PN9EG;;flNciT>%B zkMZ;0K2NAAFL3kA0Xb|IN1Orq;^XZKl9P*Ks}+^bJ*+S{DSWpxth?ZP{(e|!wX*Na z7RjZ=!R2E)4nSjGKtR~p9dDl`r;CZnQL$UV8{({=+?)dC^V6_obmx zpGZFNVinjEp0XF_)$h^_&7y`zJg5OeI%%dk$4Zh*EC2w^dr)tx`^zPW^Uh1@KPie$ zF|T*(O86`IWs7V8^`#pUFnks>zZtsQcd8ZT6pUP6O?1%7$BVtysm8kk3F>nXPJ@PES@7;N(7@0?$H{B!}i7LLH2R+Qj~ zEu8Dq%l{dW@c%e+lJlg=EzV!gb@;;n-^}wg=yLFXdjU8II0VR{TX1jyT!&=v{~H-_ zX8n)=4hevR1K>I&fI|ZK7YT3@0vsaghXoEv^^jB#UE$Cb z01N!Db*;nn^#A$v^lPQq7Y@=-Q5xF1Y($S7a{nRs|6snu4%tIjICO`~)7|r%p6;F}T2oyC3!Mxd4h{}WNl{iC4h~=d`<;b~4Eyw05mbhK zA%9R&kcE4N^MHf1q+C>lQP5lz4Zpy8Ed6uAOS5Eq!-y#EN@{W_zmSMAaH+pG&*j6x z(ZDImO6hv9p5*%Yn|!{0-P-^8BUh>8Gm|arW5Th@ToDbbQycU~M+)~AYaJI47sT|= zc16}@s~3PC=sLtTPFIc^Lo=J&%_f>!tztE6)!n%M_wkoHEr)IPx7|!7-_2V6p8oWv z+f8fXseR!opPzV`1KgM&LkN&3*Fc-_4)1;@#=OT-MhSnEe@HC-X~GoKhAcGRXPqAW zJS+9mD+oBknaYB2|I=vz@tY+{1niCW_Md;;m;?s@{QLhQFxCC1{r}tIzXVka(to@B z?}Q8p@4sC@pZ{}5%l{7nF!3)F-2ZLyU&4R6vi#o(|7G+4$oGGl_k>)q6iv{_|fJsf6QNFs?z>5K*Vo6aRE0gkr5FQ z$iQ4I2dp0ePw>kz<}n;UyiPE3LGtpWw_K_AJtg<2YMXyV!Bp3x`1@zBs0^?br3J~8 zlqt_scfk<2J&*bcE(jqAi0V%*6l7yFI+*s4E;1ll>p!yJaZ$?6BtU9_@XdTc=TxxM zP^?)wCQbH5)OQeHaAjvlhqS%xe{={ED9jY~;)DaUb%kyr zQvYt8T835t{~59bQt9aEKwlL7kJbT|r^35QGAwUmqwV}YfdW7X!3ZD(U5{*Vk}?8X zWNjIw7(xt^^z`sZt8V>QT6(5#ciWMC+}ugp3vp4$+Hc?BAY<2{gH59nZ~g7EnX52O z*y+r*7e-)XI#!(im$?NT1u7qg?(S|0SBH5s1Ox;Wguk+%s9cQ@m+~s&XAu0=WP9pu zW{LvI%l?Ni&J0m6Cu!h!KKW0O*8g9e#%z@RkG+h~Y1ssVebhKi6yZfd8Li5Ez_hzH`o# z)#4{wE^KYRZHAD8=w&}WAmZu{7$U!Yn?51gcDw1vk+ktI|LuDa1e{b^Iv%WLzL=s` zxLRgxO$`mmO=7Sp@H&+x=EdWz`*t9DA}e*;)Q_5>|ZdaYY3g#0TEd;b-` zvu;}l2Zsveie74OglRFQ)lR@Vz<=iU8RfL?7xskXeY{?7=g7fzD-#z??H z>g0h5nzgvNh~NdP9xt%QK}|4|{3bu#55#04;<^ViS-19j8C-hqohjQF_erVCO4ll6_x*Qx zohbV>CUAB>ucr0^@qyd{{5R(5w6PoM8KW6)ij_$?WCD&u+3*TQeqbtcIAW2~d%03X z?S!yE4Yo?1Qsv&a2f5s69p>L~?HnC*0Tk47GuFWYnaj=aBe8EU9Q&*Hf97_&%oNGV z{JO6Lg~M(oyo$F>w2Zy{CIf9Zm~`L`d2h#ZYA_wH-m4c;UC~1$zBjV>5BdvlHiH@K z4bsXIZ3WL3Foh=NtsilET_ws|sG#23JK(btF7=Sgbs|8Q|BVr2K{*?3Xl4)hsF+j^ z--#47@ZzdRSJ!|M)O@d)D$=N?wMz-t!BvM-XN=}4ok?~JdH|BAm%ngVQPV|->yefs zh7ok&JXZv*5QEQo4q3q>ZIBi zvDkBdeDNa%adE+6=68EP*PE0F*@N}rMD>GO*!%UOmFE!|w7zy&5tPsYjmbe^79wD+ zuBx|;Ws5 z+Dv5Cxzdy?PrGk7(<`375K7_JqQ#rZ<&=0}U(6i4?_{)Ma&i4suk5$SJwVB;;S`Gn zi7xy-WuEIba9rSxg-bSA&SV!-|{PEj@C$kWS%GW!tKEd=h<>8RuHuQZ{)ovsw|l zi#zHmH0Y*7XQj4#lEZ^I3FJ(4nu$0U)BfWXgU4_}Aw!~5Y{8hIyLC4j1X$z z9p%yUQfLgdgNZ{X1iZlHF2eXVk8TargCseyOp zknY=yyfDbhLi@x1S_Lw5$RUVb`DUXdTxrsaXxLD`16i+n+HzShC4wtYU^ zZdmkHwb~affl48D%#kXRHGlv?fPD6TAZJL{m@*e;eJEI72?3YMU0a0XAUM-v^C>P^ zr(7V*V&PBBCdee(49?!uPUE}v7YK5kX-c{8BnvZqK3zfu zc!_S={!LzQs6X))m$yiUQ)N&g5Iw7uPDCda2acNA({}0cbNmO7^czaq!0&98e0}3+ zqO1`iR$@iAK-WiG==&qdrvgkd_$QJ!ldm29lHa*$!}WM<aH@^xB5YP)tcKB@P^ef=}s4>biyVJ(QZ`sF3RR9p)E56_qB`3J#u+<_(lXn^M#Z10cSrzi$?c>nP!B^l0CAkMQ=bQppe) z{OU%VjG>!Er-oApcxDH;xJ{ZobDayw2F31It4~qq3voD8S~6BwR}1g__!ERAq2Zrk zw_|slR^4UaS-5nvw(iHFB;2r^e7z|6rd>Wv2UH0PWy)wP>L?aQzYCT1uWT#v`Tl{s zxMo;uIax(MnL?Ri32uuy;W{%4nO=#4;9dpTUew0WYHlkv7e>M*BbB00*Ph=03fGH> zZT+FlM@g4B=M2@?vFOL2SSg*2EpQE@nzo5}9XognKF~b3@%l)>0Z|=yZ%X;hBK|d}SWhHrlA7BBmeahPAICz4g1eiWo0Hq%{!)XS zasBF44dMxEGmN%#I|&&M4__e8z`bJ2BsJjB0zcFQ>c6v`60J&q&N`i)Iw*QH#PTN8 z;msZe6dBsAny~I@3+;yF7sq&gSWeCc_ky?5vDUuChukW~vrFVacH8(jJrV* z*N>fTYI=~be52k(P)_p2N=+=ISHT<{mb9@?*vK`AfolHtOpW2M|-qDcR^-bLX8d`DCiN3u}sQll{H70VP*e)^A`k7Gr5DQAxdOU5=)==wJJ5(OHd`U1v@gj)jLzSI@B|5k7&drz`^ z0|->1P~c1cLgKr?<8Di`+cHcd%r`D0|er6w-A`Vh&r?Fo8Y~a_%s= zaz9!^J`OIOMs^+({2>{*;>M%-^H#4R;r+S%-K{`L)L$V-JhR57Nw&)b(@O?%)2*6g zj+8DmF{pU?Pcrl@$yBtCeD{m#4}M=li}DdxD@DI?go}gB74UPZJa=nUTlekt(3y@$ zndJ6J;$?8H9Hj!L0Z%yK&Pd9Y*x8ftEM3`Yj0SDYJ>1lM-I1YB+trZ7_ftH!{7fH9 zn%QcMS|LmsXip^WiQG0lG?eZ*T_y`R0NB)w#NA)Ich}cn!jXSWhphw(%B8ay^#VO- z%wki1edKx-y4nLsx^&5k)jpnta(=idZu?ArQzsIo{p~2V7M&HLf+-(gYDUpA3;Yh- zVk&>G>&6?P`bTiI=FH(vqg}?kPoMb1MNk@2r%l0Ov~q()b5Pp|w=sh}duW(NOoT$2 zSfjnd*d&oWGvZc*g?gbmM|8qJpM~z7@FH@Banf1~9uS6KnXI~XkU0tPzd!cHOxE-^ z*7)^A-=Y6vV&pXH@~+3<&shEhbAYpT7HnoGK9~!i1OwKx+Q@!dhP-fNx=|~l+`G2V zQF;C@vmacFCSY{3?R&CY`p3xBYP=RY?E zR!(y;i)vCyhV6XWq-B9kDqY{g9DOT1%8pk&rLzXdKLQq= zCu&#M(Ij);a+PIzwO7uKt?SI;k5@e2)M?aA)qGBHAe^E^AC{0PCH(aL!;YO0D^xe> zXC3_?*@G~tp9)v*&>RjE6Rg(uGp#i0blIA@-x%z$_x^# ztD1Bx0)*7*drXgis8M2o1ghEU9!vwI7W@Qkk@SXjBzpC^A{`siHRdCZM?ap^hXiVd z<_50i1GGJy6JZ30(7jb1Bx$OzRHnC{tata2hYqhg;53DwtfcP&NinlzlW*az8}^W!67}YP zauMmAk$oAUeBF`()i(Y^^z?c{)74lkuvINaiOE&Vi^)Nxh-HiulqaLt#ipsTup9{? zA3^D%OYZX1`TCAofx9D(CC0?JE+VSiASI%##BE%n(r3dHi9kPlSoC7oWv9y_d;blt zGu619jIS+HM=!3$kRIm8!@`PEnh?^nsna%ZuW57Vc zM4%KD+-9dzT@H7v`;KUd?rmDC=c6@|yn735*;M(u=a#}^j8KcPspfxm;!EF+;G$Gt zd?+61H+7_iQ6+;Tp303GKyxWpyL;p)_em4T9FAhIu9L}ioNnDp`Wz*^_bz=2_s|5| zqDG)U$*LuuD@0yfAMIEpYi%>lZyu62DN%DK*(%pW7!Z7CFuu%xqq+A~SA0Kkcyl_r z3B~5HxhR*VrdKC1<+?(6MaqMtg4cuVUXb$?adqqy2N(IgS6Lm{3`a$0#``2@Q*r!Kt;Zgdvonv2J z*zYqx8>rd$+D_^F9H0RF(}b&1zm`)yTd-Ov8O2c>nsE{-+5Eo<272OR=to-VGN} zsyGgt5qL9$N8t1wZp^i#JyCjOPuM>af_e#g@_>%FXc1n&%w<~BNH)0G?QSLihOhox5eMY{EDsnD1=TN$nBz_B?OG%P>R(F zov)<_@=)_)e1ko=OfW>-^&rsapfIut=HECMVJ}zD;pMN`3&3C`#=VI1<8^a~U)P&E z`|JEi+?XQS7+)IJg5*24+hoXEaDcr{i8&WT3w$0`+6<~uR;rC}A;+S;eP!uxtjLqLa#JONZX6s49l?6p; zhstL@q73+BqmxkKh1;5DAq|ATv6L&l4=3T@-?ih2d|{0j%3OUj#RaU+HN7Piy zv+ROxi3aB@ZNXZ{e{9o3weTEe-_S$c87?BY1Ex>cUYG-a+!VNl%fJ3QBdN206*nf$ z3MUoFg6XyteXs$Z#rCIo_ip6Ndutw>z*=heTSr}s$t;1`mlvVuG!-1Zv{N)8GkTD* zM7Q;sTJ1*7xst|mGxpYn64WHgp3pE8P0z`}LJ|*2Er@g5wCLOl)EL}tf4;c}UyB^r zUT=Pk)q-E@{d}N{Vf}Gi;b`-jF5ba)W-$}h$Xt}pLNFyf50ceZz?%{nV~@5 ztcR3l*RWl4L)*x1rGmPWlk(|13~2J_fuUyHZipesqFNt3jD8_bJmQXcP{=rj{`HdC zBkZQ}>&P)&<`=E%;2K4-zdKEYvu1M()wq5CHb-zobVj_SPu(kg_7P318(oMx_R#NM zR5C48N>Vpe;T7R7iis_; zsM&SfEs!y$M#SwgZ!_$BuU48f9x-J##s$g~p3uszO5&Jdck!tP*MU|+r?=i|&_a4I z$Smx!auYyN-PdHUGTkYql~L40+4y5e!S+sWQ5%6Z@xmpG&Kb;2K5sNvRvgg=P7 z>3vS%hc2DLl3>1m8n2HcqqQWi;--IbLPNRcN9SOx1V+^!fkhnK1?i;^1P_vU+Vgn2 zpd+GZr;L#ZppN&N^0>$g(%cQrQWWc8g?R%nK0eo(Erff6U}-OLrr(50j@*(HkdPeI zuKc?rCyv#7st+1lq?rYe$ulvP$)insv4MUsQb|+v4>dq$hV=g zQpJ4T5!CAy*p8Zy@?!eyw%a^xiZ#vO>Ln(-N2_&RcVSnnE2+q>*@97tKPNf00)_oEZMSBu=c3RQ)=P~?NAZsmPAU0+ z6e`4O&hemGWKmW6aL%(9^jLUH31Yb3jvRw&F?qN?X^(T1;XG%ds+u&_;Y18qpLR+r zYUK3z#v=z3NXfUvWOvKep4KE~uVMp}rbI8)oAy)~c9o&BJN!7qHV#6^O8%~6L_`HT z(`&PTJM9~4zOVf)j533J8tVIXgBEr8zSco zkx!lOVdk9}!*U;-SN9;Da=S?^F`UORgt=KB=h=qsjgyUs6HN}VZ}MCg8T>5&hrbp z_F*iIZwpF0-aJdKU3qX4Zy0yCBkn7QPiXOjUt{{=E@tL6>}lM$LYV{)h8F&o!Mh>W zHU9GQL%(xSo|wqt%=z2N9`lrS7qLwjNxT0{Bf1GK}LU*tw(wlMjlo9BZ$!ZGojpH+1*`J&y_tdQ&ZCo z*p&u`u3xE(U)x3z{1HEE!^$Vxb`D&-GZ~HR7I33>=^b#+0&?o{(w~eo2$FF+SEmJk zkvj7rszZaKh%>NA%I1j8e#X0DEqci7L!zhF!s2w0PJOo{7?}=8x25Z#-N^H50VPAx z@Qc{-)LrYy&Hl^)M#7~$W$O8ap;!rylhjBxuoQGwxW^qVL{~seKwU^qul9`^Fk0C5 z*NM&LGn-a>r&)tIbLbx4MSTCr@S_&`NI2z6bJJXhG#es5_-J)c(vM-{E8*wI3feUd ztjtc}x*u5De2ku&>5!d?RS!RvU8z5YTQ^xK3Uibp)m>W2muPkjJ-&jsT{ZKtVtlK| z^;S^f^*53V~gvWFt(x>Z)BrwC4d#H>xzX2 zp$n!088qBXl}u&bO>|1#^O z!)I^N$Eek4sAn?Zm^UDbsW*p7Ah2nzXg`l(3C`A&+_q`d~L6DM}f* zq$@8}qt~U6i}Inf2uElN2mQI37ys}kjn<|9BhI))5xqIxc`DP6J7WzC0`d1X^r?j^ zoKy0V?c&ikOi zVZXM`)k4|>`Du30MGK`dJB7%T+jIw@vX`ySgjv|#qq~ja{A`9WodV&F=o^Dq&ret1 zpq$ICmZl(}h-7Smg3u=7I)m9&q7gZI{I9yeomoNnTGD&d5+WPVJI-6bgIfCyl$YaE zCn$Mae7(QS{nH^=d_BGrLZpa)Vj$G-kjA`By*z@`1Zmyz>k;E|bmuHE0J3+vmu8fR zl{|h<=+YRf&@Emf+$;gwTMsSW%45U!vXCPRSf6RkV@R<5BE=9=d3J<+nUI%e;uR+^ zr-Yk6<4QCyiuwd`{=>Sks`x3yAL)+N=`S2573XL*qZ^8-amgx54d;L?q_m=7TIY{M z5&`_S$WB&XjZJ+2ZXDibS$juR#|Jz#Q`OvL><+(RYUs;1>ka(8lw!6X)5#f>^CSJ; z@k}JSwyzf&g;z#WjeiIXK11|@Ud?UDPN=`l~i2 z_1gr*`yNBx-ZPFKA6dH&@Sw<%8tV#d_SD;cTB(S?g>zapLSM|g-K7k>)8dNMe;fGH zxraSOg`6gK&LJZOCg+)#FR32A;~w8RHwJu775Fm0Ab)jx*y7)9z0$kXN3xROZC1lk z_fH`Kjt$aRI=8w;4LuK^9CJL4+#DR`<9nC1@1?=TvQcz|l)jpu5czufPGc_ETs%6e zzdK!?l&ty2Ko;6Wi&hQ84gXvzXSK-2J0vfKe0pD9m-OWZlslbgM~!v_ zFWdLCEPnAcZjA|f=F)9OAtIGm1!70T2E7pYf!84d{K1jX8o_d$*pcFEyXEhB-6nY7 z^EQ0HBY9ouPl1KRdAGzGNk<|#cA#3dF@qD3)p`w4xJSKL9|gP>v?DKR#}6Y2sI@LHDn0@Ua$PH|diWQ)0Iu;NryTjfUA0URH> zECu@czQojR`qC_zp~gy`nhy#)nbaBcWbE8a1(oy>#<)_q;kPHsj@j65M6X*QStdP- zy2QYEGKlVyH>>JPhnSvzS6;9;I=jN%vf=~nc_#dnvJV<)O|X$(!7@07hm|rgfdPMz z$bZIez8xsf_P|5a1YU=ZdKCwa z1Q0P<12A$)>LtZbt&h7NcC~K%n9gmGRmWWO)i0Y}KELe*j%?=j#3DOyB$7%!)+s7M zpJBLw6#YwB7tyQK5Lqg}d@BHavvbxOmO2l4Nca zkNy>hoBB%z7A@K4SI~mR4^p(abQJ}5ID0-sh0QhaXVXf!qUA5h-HOqf zPM97*Q^3VU4lLW|9GQuk&t$J6oRnRSO=x(^;Rys_FzmtWvZejT^OTtjuCB_B^QGOE z(=k0=eb$}VZ+)Zdj_P$kT6VtUnhLFH0+#Po*+H06#Mr6f*(oI4+Ugt!O)1vh;?Tlk z2Go*xc#YXt53u+Octf;uKc9S>$4LI_*;alvks036?P{b|8`nuvMQHD*V&a20k#}H` zi!)&sId!>oNTC14689s37TS|nSO4<`V|kTa&~Rx8!Lz}W_h{IGMJX~?*)%=ioIor5 zqPDU*0#Wfp#EC-!;#pG1c^fl}3JKZN$QPUakk22;UAcm{TCBg7oonl4ogZ1H zBG3nI*fs?^%cS*+7`tRSEBY=SBIpAqI7QXOjz37BWS(NJk&5&5bA5v483!Tp3p?>Jc~yTN{vzVMw)dYN|VqJ?@r~HmG_jl zAbDOQFRChutD%m_yd$^uAxky(&dLIdxOm7>y;Zrej#%*LiEnA3NBO((lBheFf+$I` z^_#cK*p3ZGu<8*`@dG)DE6f&%f$M3J7uThXT}RPO>!yfCKkMAl$oz;*eORpuJhyH8 zp-N$lk<}>LO2O87J`pR8)?8pti-B4IbH%H%*zmXF8eIr*W<>yLy0%gw%`hyKLgMnC z1xN)rpJ3!*da_oiBl;DhTRFSL+PTAt?L=z7Ukrv*i4MQ<=F1gg-VwJ7Kg!29OQrK& zoC%;|#O7&8rR8Gur42R(zS7f8)ilzpXzwpA#Bc3l@+2~6TXE?1uvP(su5dOuI+$b`{fr)_dh;60REU8pz5D%OBK6U>PQU|Q2 zJ8vs?O@hNlda3=X7N%M3?|3~p-$E27<-~e{cj;}e{pXDkk(VU`!5D3wzxS_Nzze#+ z#a&iJ=c;o=z0^|+RX@$G;(|-tdDW@;H-|Uq;Ue}inGg_m{_U2>%d=%9z}{Hu#$p#Y zVl{eQvrJbmlATJunaC`zJ*Eql%Xf6!>eu;f|8L!+oKumWm)ag-L^I8-#By>_ny$fD z>`T+dBYGjE#is#SabPUunv1jn{9*FbeDa=mQuAN<_&+;T6%;0s)!f8h7nY2&=~a$| zyISi*ub+s_bRq-l_@_GIj`O$XeTS9LD6d)NTVvXT)muC&%ug|gs( z)W6w^f`Bi*7O^de@aFX8DNnSv@gQjmtO#AEy+Fw=$>o(Cz-DM?ang59!Cg4){yO>P;@P2@IZO9B8+SCi#c;5uW#B)oI^m%rR7DlzEq36^xIHBhlJLj{><95ZkF zjm*AA+@zteCy1VnVTR-2y#2A@pI$}WQgAMBVIG-idaP8+-A_jbcy+&{LP75WQ=Lfb zM&HB|`y$Yk&ap#s+baBd2)u*D)8ABLfQU4V+H;9FUWwM<5DCO#ktZ}>mY;jA4-!{p z(&xhh@Epzg)eY<}MFY%lkep1Xu+cowmIu-U9Yh-<7v@~}(wp&Z0ir5j@7(DnvK@ce?ZtchKSFPRcT{7bHkr?j)R3V<^nHBHjlfX>Z#=H>}>6^D5*cQn_EX zUp%cZLfq(26VSw=^B;n2YqLnkF&fIp$%7L` z1Ft62N)o&aX<}6NpYXh{&*2hoq1OWZ-~15%y8h|zz(}@U6cv=s_jv10PPiH)rDN(H z8>8apS6~v1)Y|Lb6@#g}UydQl3SrjK#cui*l)O7Su1}4ZOddV87Q3h_FP`i)r6419 zY6sFFur~4P6NKDbDomikwo3u(#L1eTaGdU4QJAz(9Tz!IQN!)DEZpcQQ*?JJ;E|D51N-YZxv#z$~)m2~Cx}av8!1G!o{R8BkT<2^ z6!{XLvPpJHZT}6$urgWZ4g%_OO==#E3d)Z&lI`QVOOdvX83*-_N`gdX8Je{0=E=?2 zj!)J?Q4?-5p)L9WY%uo;~; zyi%7N$My^XVpC@#{yb-kZe5(K?pNGX9Lb&^dGBm^Mpp=~1YVC=l4PMwl#4z!qOk{_Q!o+uPf0>_tFwKK|p&@UtW$7cp_2W5pe+pEB zx-VQ184fkN5Dx$OtF!YB%mWJhjCh@ouH z3h*P@Eekfoq}V;LpQ86o+cJw1^#0)rD;0!Q@*aJQXgO8fc3b2;+l{G!tU!SHjMyxk z4i{!MDq@%o(b_y7{G(Wdnf>;(HFS$SLH;9~vWYGb)J_*=6o0lGiMG`$Kw8=BT% z8qU@YxoUAl8qAW9`=qZ#ch;44?9NRq5ubg*jtk`&?CA@CP{nJY0Fr|jn zgHCZ~kQ8JN%nQMr&&XTQYQ5mRZ-$R+|ghd9i-hMy2d+J*Hbl)V#64rb|u;k=v$!A4mfEsT?_RQ(;>_Ip2T6!n!eqWSDzY>+Ppj zxjVEAyxxiBu@Ske2Gwg%$JvNHo@84GW72S`zk3DLkY}|{fhJrOsUY37Uj1oF9HjO& zu`8Y=+Hl49*F%&+(iY(Uyi{T76Y?n}gIkk$!G#h@w8f`lecA$*!Ctw1u3b#~EuA9qEP>;{NITauP zeqXCQ>~(IYhZhH7Me+l#^2z#DvTc3@Z}J0UBx$*a=-7)JmMeX)2c6(I;U|@`7FiEb z%!f#;&nx$n4gP^fkFhcha6*_~hXDEcbyzCPJ~&Be@`p~r{T(^5G9XEA1+S@o73CT_ z$%SFUK$Yn9tzXPf7>4jzcZ}!TM%8?CV>=HI!Zb4Jh$-Q1UZ62)}GLGVd3n?}vRPRo6NgCd?PWLR3Q^6ULk9FWYD z{}q3aa=t7$03bxshJ1CaR76c2x}MA0%THr{?KGRQL@n*(qeBPwo{N#8!1UMr<(#EQ z|JaL}VXn@&sOUG;=@>$1;!nd3^35M2h?5%Takd$apC=8!x?yM)Hg?W!hNUWc1u>wjL zp7&X1(H)G7muwId5PVwudt-*?Jz$Q21|X3hQ(l=zrboNFIXbXfDh45wA^GmTQP{Vg zFBFNq6 z1Y#0c!};NvF9-^%OCI0 zeU7l6Mng0}7lIjqjvY2)+j%*FS(=#47`W%W0=Xgl-ddtHHH8%|Je;%UoLU%pQwO#x{ zUV`brHRTeXBkT$4JuL9EU>VJ09esi1E&doL!t!P#q+;&b$Z9PUT*QOQfdrvUb}2hnyyv(sU#WM~YtM*b2f0$Fyqmmml%;!B zP4Xwd?)9XiyM)s?UZw$m(Miuq^@N;r?7ThuY=4lc?%FDr4?5nn!6*N=%I`M-sw$Q=&S|GDv;_YbbB8(9rRrzXl(BUwmoFxNxhxN9|E zmeW)u&0i38_NZ@kTL-0vDT8b$Yr8*<=A{CHIRH!S~$2$sBbpJ4(nAbIFZ?d~6 z>fP7+dUJl5>W#Ed!ii%1o7ftDq!9Vl1hV#9!&1e~1PD~_I0ZS_CUzK47&}LTeUHJ^ zlLkkhs1U^&rKSW5?c>B0{=U+3PF@E$56k=DZ-0E*yzfn5>feMNN~;({+?YUw6&p-` zqf7xZC8#_$ZRK$Z2|y|D17$U6)yXy2`-c{&@e(u~yD!nu@?35#Bv~-g{a0ka)l(&3 z2h)up#BwLTKHTBBbZzCmrcj7>NLm0UGOq5V0vX4dgU_4`F`j5#s78rltOC_Az5OgT zlNynJ6IjP@;Uk@vMg?+j|oZeG3R07JC_p z4mJjPm4-f1W`8B`NBma*`FL6cCVg==s6&A@#>3SfM2YIHD#`{zGf~HerR?4ZsLr4( zrZIZ=d-Bn-!y9QYwS6hSK)Br>cL7Xk)6dMX(z|*3;ok{~1?MFNBTjwUGahK5{POcW z_oUaevKLDNgW+!<#a1ya6_4=4K1OeJr)&@fG8Z}P&#-Gt^>FUw^Y(#bsCR>Ci<}Xh z)&QR4I0$FAQ9VZXpvN7$RZ2U$3xS=xL1t#}N49f)=rfH*W*T{Hyk;TFt_m5(;(iOX z%m6(nYIL9P*SOc}^^FMtB+gM$~Hr2n-+9lDN+=TkCx17Y{pG=}_3&^rVBD4=({HHoALm z#p;4`?vzwp68dy$H24s=8MWV;X45k$XY@M0dtt>v%PI0wp=!K+5%IkBoMp5Y1uR#Xyh*i_Ga5nfbQb{8w>&E?v^TAK2dk|tz26e)XE9O!o& zv^XyVGi;LV4oK^Mq*&1wjW40=;>AAm`Mwx@PRO}u{*xLEE2K+gM-j!-2=|PUHnDP4 z{YwP343XXGD1ge0L7CW>9{KA!>S6AFJ^Ce=jzpWNPq+rak=iHB>4bCNptC2gxxW~C ztQKM;BZm{`Ykwy%Lgy&q1JI9OZht>4z8)iZmI84Zl%iJAn6YX)0(h z)}EU6oY27H(OkiZESA==|$!}J6Ebu6*-}IYFoiIm!fRq z_x)SdyC?0#0!_dvnM@JUN&0OZ?@PV^42y$KSaycAzWt9?>8@_gxDka@3SbXbSHg0!2g9^x zBzaMk&=b@M>IDf%W9W|Gk8@sa%6b>#>2wyZ^}6Qz%Z}m_Kdtf^R@x8%#qn(7kCJyr zE{nTuMD#^;x}r20!Vzs$ir;O9=wUyw6J9;IYaZ0D+&vKyzUHc6i0A;ZkzmKO5Ik#~ zG`n2+QRQ}ulh=Z?kzS&wPg?g|+f>&rtp$}BCL!x~)D+mqNO!P;-_IX37EU0OsI7%! z`Pi4nCJ6Zw(N}9zNJ#fCeXXK60g|D9j9jSDJ!K&1Zjp+3L9`!%X!ra zSc9toOI<7)+I(6DG@eQm;F${t#RxH3ptDMmYBBOjKFury)IsmXfqERVo||+&>>4>JgCPup!XHcN?{=d zAM^QjC{_R=6SzGv7to5#wdJ+rBlftP)Fc;yeRno3oxTLvoi1+sk>he_NV}sf1V4F z2%{hjwi4^Zvqudu<}s>MUWCPnomOz_pi0Fa7D%|gJ_og1a(*%e43EM3z$wY8%hpI+ Gg#0hkRK

diff --git a/src/core/login/pages/init/init.scss b/src/core/login/pages/init/init.scss index 79f1005d0bd..f456779df55 100644 --- a/src/core/login/pages/init/init.scss +++ b/src/core/login/pages/init/init.scss @@ -1,34 +1,30 @@ +$core-splash-bgsize: 100vmax !default; +$core-splash-spinner-color: $core-init-screen-spinner-color !default; +$core-splash-bgcolor: $core-color-init-screen !default; + ion-app.app-root page-core-login-init { .scroll-content { - background-color: $core-color-init-screen; /* Change this to add a bg image or change color */ - background: -webkit-radial-gradient($core-color-init-screen-alt, $core-color-init-screen); - background: radial-gradient($core-color-init-screen-alt, $core-color-init-screen); - background-repeat: no-repeat; - background-position: center center; - } - .core-bglogo { + background: $core-splash-bgcolor; /* Change this to add a bg image or change color */ + overflow: hidden; position: absolute; @include position(0, 0, 0, 0); height: 100%; width: 100%; display: table; + } + .core-bglogo { + display: table-cell; + text-align: center; + vertical-align: middle; - .core-logo { - display: table-cell; - text-align: center; - vertical-align: middle; - } - - img { - width: $core-init-screen-logo-width; - max-width: $core-init-screen-logo-max-width; - display: block; - margin: 0 auto; - margin-bottom: 30px; - } + background-image: url('/assets/img/splash.png'); + background-repeat: no-repeat; + background-size: 100%; + background-size: $core-splash-bgsize; + background-position: center; .spinner circle, .spinner line { - stroke: $core-init-screen-spinner-color; + stroke: $core-splash-spinner-color; } } } diff --git a/src/theme/variables.scss b/src/theme/variables.scss index 36a6110c366..919afa355da 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -133,10 +133,7 @@ $core-star-color: $core-color !default; // Init screen. $core-color-init-screen: #ffffff !default; -$core-color-init-screen-alt: #ffffff !default; $core-init-screen-spinner-color: $core-color !default; -$core-init-screen-logo-width: 60% !default; -$core-init-screen-logo-max-width: 300px !default; $core-fixed-url: false !default; From 04ebb8e50ca3b67076c1d95e821f4ba80eeb3b9f Mon Sep 17 00:00:00 2001 From: Mark Johnson Date: Fri, 12 Apr 2019 14:37:45 +0100 Subject: [PATCH 006/241] MOBILE-2735 UX: Course and module links add new page to history. This ensures that "back" always goes back and "home" always goes home. --- src/addon/mod/book/providers/link-handler.ts | 2 +- .../lesson/providers/grade-link-handler.ts | 2 +- .../lesson/providers/index-link-handler.ts | 15 ++++++++----- .../classes/module-grade-handler.ts | 2 +- .../classes/module-index-handler.ts | 2 +- src/core/course/providers/helper.ts | 22 +++++++++++++++++-- .../courses/providers/course-link-handler.ts | 19 ++++++++++++---- 7 files changed, 49 insertions(+), 15 deletions(-) diff --git a/src/addon/mod/book/providers/link-handler.ts b/src/addon/mod/book/providers/link-handler.ts index 899978d4b90..631885cbeca 100644 --- a/src/addon/mod/book/providers/link-handler.ts +++ b/src/addon/mod/book/providers/link-handler.ts @@ -46,7 +46,7 @@ export class AddonModBookLinkHandler extends CoreContentLinksModuleIndexHandler return [{ action: (siteId, navCtrl?): void => { this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId, undefined, - this.useModNameToGetModule ? this.modName : undefined, modParams); + this.useModNameToGetModule ? this.modName : undefined, modParams, navCtrl); } }]; } diff --git a/src/addon/mod/lesson/providers/grade-link-handler.ts b/src/addon/mod/lesson/providers/grade-link-handler.ts index f71c50586c6..f76eb8b52ed 100644 --- a/src/addon/mod/lesson/providers/grade-link-handler.ts +++ b/src/addon/mod/lesson/providers/grade-link-handler.ts @@ -70,7 +70,7 @@ export class AddonModLessonGradeLinkHandler extends CoreContentLinksModuleGradeH this.linkHelper.goInSite(navCtrl, 'AddonModLessonUserRetakePage', pageParams, siteId); } else { // User cannot view the report, go to lesson index. - this.courseHelper.navigateToModule(moduleId, siteId, courseId, module.section); + this.courseHelper.navigateToModule(moduleId, siteId, courseId, module.section, undefined, undefined, navCtrl); } }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); diff --git a/src/addon/mod/lesson/providers/index-link-handler.ts b/src/addon/mod/lesson/providers/index-link-handler.ts index 244c55fcd5b..16d77cd6e40 100644 --- a/src/addon/mod/lesson/providers/index-link-handler.ts +++ b/src/addon/mod/lesson/providers/index-link-handler.ts @@ -19,6 +19,7 @@ import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { AddonModLessonProvider } from './lesson'; +import { NavController } from 'ionic-angular'; /** * Handler to treat links to lesson index. @@ -51,9 +52,10 @@ export class AddonModLessonIndexLinkHandler extends CoreContentLinksModuleIndexH /* Ignore the pageid param. If we open the lesson player with a certain page and the user hasn't started the lesson, an error is thrown: could not find lesson_timer records. */ if (params.userpassword) { - this.navigateToModuleWithPassword(parseInt(params.id, 10), courseId, params.userpassword, siteId); + this.navigateToModuleWithPassword(parseInt(params.id, 10), courseId, params.userpassword, siteId, navCtrl); } else { - this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId); + this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId, + undefined, undefined, undefined, navCtrl); } } }]; @@ -80,9 +82,11 @@ export class AddonModLessonIndexLinkHandler extends CoreContentLinksModuleIndexH * @param {number} courseId Course ID. * @param {string} password Password. * @param {string} siteId Site ID. + * @param {NavController} navCtrl Navigation controller. * @return {Promise} Promise resolved when navigated. */ - protected navigateToModuleWithPassword(moduleId: number, courseId: number, password: string, siteId: string): Promise { + protected navigateToModuleWithPassword(moduleId: number, courseId: number, password: string, siteId: string, + navCtrl?: NavController): Promise { const modal = this.domUtils.showModalLoading(); // Get the module. @@ -93,11 +97,12 @@ export class AddonModLessonIndexLinkHandler extends CoreContentLinksModuleIndexH return this.lessonProvider.storePassword(parseInt(module.instance, 10), password, siteId).catch(() => { // Ignore errors. }).then(() => { - return this.courseHelper.navigateToModule(moduleId, siteId, courseId, module.section); + return this.courseHelper.navigateToModule(moduleId, siteId, courseId, module.section, + undefined, undefined, navCtrl); }); }).catch(() => { // Error, go to index page. - return this.courseHelper.navigateToModule(moduleId, siteId, courseId); + return this.courseHelper.navigateToModule(moduleId, siteId, courseId, undefined, undefined, undefined, navCtrl); }).finally(() => { modal.dismiss(); }); diff --git a/src/core/contentlinks/classes/module-grade-handler.ts b/src/core/contentlinks/classes/module-grade-handler.ts index a97b30a2a71..c1a5124e4fd 100644 --- a/src/core/contentlinks/classes/module-grade-handler.ts +++ b/src/core/contentlinks/classes/module-grade-handler.ts @@ -77,7 +77,7 @@ export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerB if (!params.userid || params.userid == site.getUserId()) { // No user specified or current user. Navigate to module. this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId, undefined, - this.useModNameToGetModule ? this.modName : undefined); + this.useModNameToGetModule ? this.modName : undefined, undefined, navCtrl); } else if (this.canReview) { // Use the goToReview function. this.goToReview(url, params, courseId, siteId, navCtrl); diff --git a/src/core/contentlinks/classes/module-index-handler.ts b/src/core/contentlinks/classes/module-index-handler.ts index fbb65afba45..67159b01258 100644 --- a/src/core/contentlinks/classes/module-index-handler.ts +++ b/src/core/contentlinks/classes/module-index-handler.ts @@ -60,7 +60,7 @@ export class CoreContentLinksModuleIndexHandler extends CoreContentLinksHandlerB return [{ action: (siteId, navCtrl?): void => { this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId, undefined, - this.useModNameToGetModule ? this.modName : undefined); + this.useModNameToGetModule ? this.modName : undefined, undefined, navCtrl); } }]; } diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index 0a7b95247f0..04a9784238a 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -36,6 +36,7 @@ import { CoreCourseModulePrefetchDelegate } from './module-prefetch-delegate'; import { CoreLoginHelperProvider } from '@core/login/providers/helper'; import { CoreConstants } from '@core/constants'; import { CoreSite } from '@classes/site'; +import { CoreLoggerProvider } from '@providers/logger'; import * as moment from 'moment'; /** @@ -115,6 +116,7 @@ export type CoreCourseCoursesProgress = { export class CoreCourseHelperProvider { protected courseDwnPromises: { [s: string]: { [id: number]: Promise } } = {}; + protected logger; constructor(private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider, private moduleDelegate: CoreCourseModuleDelegate, private prefetchDelegate: CoreCourseModulePrefetchDelegate, @@ -124,7 +126,10 @@ export class CoreCourseHelperProvider { private courseOptionsDelegate: CoreCourseOptionsDelegate, private siteHomeProvider: CoreSiteHomeProvider, private eventsProvider: CoreEventsProvider, private fileHelper: CoreFileHelperProvider, private appProvider: CoreAppProvider, private fileProvider: CoreFileProvider, private injector: Injector, - private coursesProvider: CoreCoursesProvider, private courseOffline: CoreCourseOfflineProvider) { } + private coursesProvider: CoreCoursesProvider, private courseOffline: CoreCourseOfflineProvider, + private loggerProvider: CoreLoggerProvider) { + this.logger = loggerProvider.getInstance('CoreCourseHelperProvider'); + } /** * This function treats every module on the sections provided to load the handler data, treat completion @@ -1109,9 +1114,12 @@ export class CoreCourseHelperProvider { * @param {string} [modName] If set, the app will retrieve all modules of this type with a single WS call. This reduces the * number of WS calls, but it isn't recommended for modules that can return a lot of contents. * @param {any} [modParams] Params to pass to the module + * @param {NavController} [navCtrl] NavController for adding new pages to the current history. Optional for legacy support, but + * generates a warning if omitted. * @return {Promise} Promise resolved when done. */ - navigateToModule(moduleId: number, siteId?: string, courseId?: number, sectionId?: number, modName?: string, modParams?: any) + navigateToModule(moduleId: number, siteId?: string, courseId?: number, sectionId?: number, modName?: string, modParams?: any, + navCtrl?: NavController) : Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); @@ -1157,6 +1165,16 @@ export class CoreCourseHelperProvider { module.handlerData = this.moduleDelegate.getModuleDataFor(module.modname, module, courseId, sectionId); + if (navCtrl) { + // If the link handler for this module passed through navCtrl, we can use the module's handler to navigate cleanly. + // Otherwise, we will redirect below. + modal.dismiss(); + + return module.handlerData.action(event, navCtrl, module, courseId); + } + + this.logger.warn('navCtrl was not passed to navigateToModule by the link handler for ' + module.modname); + if (courseId == site.getSiteHomeId()) { // Check if site home is available. return this.siteHomeProvider.isAvailable().then(() => { diff --git a/src/core/courses/providers/course-link-handler.ts b/src/core/courses/providers/course-link-handler.ts index 010dcdbb202..dc688e1c414 100644 --- a/src/core/courses/providers/course-link-handler.ts +++ b/src/core/courses/providers/course-link-handler.ts @@ -22,6 +22,8 @@ import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreCoursesProvider } from './courses'; +import { NavController } from 'ionic-angular'; +import { CoreLoggerProvider } from '@providers/logger'; /** * Handler to treat links to course view or enrol (except site home). @@ -32,12 +34,15 @@ export class CoreCoursesCourseLinkHandler extends CoreContentLinksHandlerBase { pattern = /((\/enrol\/index\.php)|(\/course\/enrol\.php)|(\/course\/view\.php)).*([\?\&]id=\d+)/; protected waitStart = 0; + protected logger; constructor(private sitesProvider: CoreSitesProvider, private coursesProvider: CoreCoursesProvider, private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private courseProvider: CoreCourseProvider, - private textUtils: CoreTextUtilsProvider, private courseHelper: CoreCourseHelperProvider) { + private textUtils: CoreTextUtilsProvider, private courseHelper: CoreCourseHelperProvider, + private loggerProvider: CoreLoggerProvider) { super(); + this.logger = loggerProvider.getInstance('CoreCoursesCourseLinkHandler'); } /** @@ -75,7 +80,7 @@ export class CoreCoursesCourseLinkHandler extends CoreContentLinksHandlerBase { action: (siteId, navCtrl?): void => { siteId = siteId || this.sitesProvider.getCurrentSiteId(); if (siteId == this.sitesProvider.getCurrentSiteId()) { - this.actionEnrol(courseId, url, pageParams).catch(() => { + this.actionEnrol(courseId, url, pageParams, navCtrl).catch(() => { // Ignore errors. }); } else { @@ -115,9 +120,11 @@ export class CoreCoursesCourseLinkHandler extends CoreContentLinksHandlerBase { * @param {number} courseId Course ID. * @param {string} url Treated URL. * @param {any} pageParams Params to send to the new page. + * @param {NavController} [navCtrl] NavController for adding new pages to the current history. Optional for legacy support, but + * generates a warning if omitted. * @return {Promise} Promise resolved when done. */ - protected actionEnrol(courseId: number, url: string, pageParams: any): Promise { + protected actionEnrol(courseId: number, url: string, pageParams: any, navCtrl?: NavController): Promise { const modal = this.domUtils.showModalLoading(), isEnrolUrl = !!url.match(/(\/enrol\/index\.php)|(\/course\/enrol\.php)/); let course; @@ -188,8 +195,12 @@ export class CoreCoursesCourseLinkHandler extends CoreContentLinksHandlerBase { }).then((course) => { modal.dismiss(); + if (typeof navCtrl === 'undefined') { + this.logger.warn('navCtrl was not passed to actionEnrol'); + } + // Now open the course. - this.courseHelper.openCourse(undefined, course, pageParams); + this.courseHelper.openCourse(navCtrl, course, pageParams); }); } From c7864512801866c40a359093f6990e4ecf94cfda Mon Sep 17 00:00:00 2001 From: Mark Johnson Date: Tue, 25 Jun 2019 13:46:33 +0100 Subject: [PATCH 007/241] MOBILE-3088 context menu: Remove grey bar below menu items --- src/components/context-menu/context-menu.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/context-menu/context-menu.scss b/src/components/context-menu/context-menu.scss index 138e4b4c727..43c1468e5f7 100644 --- a/src/components/context-menu/context-menu.scss +++ b/src/components/context-menu/context-menu.scss @@ -1,4 +1,7 @@ ion-app.app-root core-context-menu-popover { + .list { + margin-bottom: 0; + } .item-md ion-icon[item-start] + .item-inner, .item-md ion-icon[item-start] + .item-input { @include margin-horizontal(5px, null); From 08322a299dc92dfa87c9f02312b32036ea7c05bc Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 18 Jun 2019 09:50:14 +0200 Subject: [PATCH 008/241] MOBILE-2735 ux: Make all link handlers open a new page --- .../badges/providers/badge-link-handler.ts | 7 ++- .../badges/providers/mybadges-link-handler.ts | 7 ++- .../blog/providers/index-link-handler.ts | 7 ++- src/addon/blog/providers/user-handler.ts | 2 +- .../providers/plans-link-handler.ts | 8 ++-- .../competency/providers/user-handler.ts | 4 +- .../providers/contact-request-link-handler.ts | 1 - .../providers/discussion-link-handler.ts | 1 - .../messages/providers/index-link-handler.ts | 1 - .../providers/user-send-message-handler.ts | 1 - src/addon/notes/providers/user-handler.ts | 2 +- .../classes/module-list-handler.ts | 1 - src/core/contentlinks/providers/helper.ts | 25 +++++++++-- .../course/classes/main-resource-component.ts | 1 - src/core/course/components/format/format.ts | 28 ++++++++++-- src/core/course/pages/section/section.ts | 26 +++++++++-- src/core/course/providers/course.ts | 30 +++++++++++++ src/core/course/providers/helper.ts | 21 ++++----- .../courses/providers/course-link-handler.ts | 17 +++++-- .../providers/courses-index-link-handler.ts | 7 ++- .../providers/dashboard-link-handler.ts | 2 +- src/core/grades/providers/helper.ts | 16 ++++--- .../grades/providers/overview-link-handler.ts | 1 - src/core/grades/providers/user-handler.ts | 1 - src/core/login/providers/helper.ts | 2 +- src/core/mainmenu/providers/mainmenu.ts | 45 ++++++++++++++++++- .../sitehome/providers/index-link-handler.ts | 7 ++- .../providers/participants-link-handler.ts | 19 +++++--- src/core/user/providers/user-link-handler.ts | 1 - src/providers/events.ts | 1 + 30 files changed, 219 insertions(+), 73 deletions(-) diff --git a/src/addon/badges/providers/badge-link-handler.ts b/src/addon/badges/providers/badge-link-handler.ts index bcaf759e0c1..144acad5cd1 100644 --- a/src/addon/badges/providers/badge-link-handler.ts +++ b/src/addon/badges/providers/badge-link-handler.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; -import { CoreLoginHelperProvider } from '@core/login/providers/helper'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { AddonBadgesProvider } from './badges'; /** @@ -26,7 +26,7 @@ export class AddonBadgesBadgeLinkHandler extends CoreContentLinksHandlerBase { name = 'AddonBadgesBadgeLinkHandler'; pattern = /\/badges\/badge\.php.*([\?\&]hash=)/; - constructor(private badgesProvider: AddonBadgesProvider, private loginHelper: CoreLoginHelperProvider) { + constructor(private badgesProvider: AddonBadgesProvider, private linkHelper: CoreContentLinksHelperProvider) { super(); } @@ -44,8 +44,7 @@ export class AddonBadgesBadgeLinkHandler extends CoreContentLinksHandlerBase { return [{ action: (siteId, navCtrl?): void => { - // Always use redirect to make it the new history root (to avoid "loops" in history). - this.loginHelper.redirect('AddonBadgesIssuedBadgePage', {courseId: 0, badgeHash: params.hash}, siteId); + this.linkHelper.goInSite(navCtrl, 'AddonBadgesIssuedBadgePage', {courseId: 0, badgeHash: params.hash}, siteId); } }]; } diff --git a/src/addon/badges/providers/mybadges-link-handler.ts b/src/addon/badges/providers/mybadges-link-handler.ts index 8a36cbbc0df..190e575acbb 100644 --- a/src/addon/badges/providers/mybadges-link-handler.ts +++ b/src/addon/badges/providers/mybadges-link-handler.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; -import { CoreLoginHelperProvider } from '@core/login/providers/helper'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { AddonBadgesProvider } from './badges'; /** @@ -27,7 +27,7 @@ export class AddonBadgesMyBadgesLinkHandler extends CoreContentLinksHandlerBase featureName = 'CoreUserDelegate_AddonBadges'; pattern = /\/badges\/mybadges\.php/; - constructor(private badgesProvider: AddonBadgesProvider, private loginHelper: CoreLoginHelperProvider) { + constructor(private badgesProvider: AddonBadgesProvider, private linkHelper: CoreContentLinksHelperProvider) { super(); } @@ -45,8 +45,7 @@ export class AddonBadgesMyBadgesLinkHandler extends CoreContentLinksHandlerBase return [{ action: (siteId, navCtrl?): void => { - // Always use redirect to make it the new history root (to avoid "loops" in history). - this.loginHelper.redirect('AddonBadgesUserBadgesPage', {}, siteId); + this.linkHelper.goInSite(navCtrl, 'AddonBadgesUserBadgesPage', {}, siteId); } }]; } diff --git a/src/addon/blog/providers/index-link-handler.ts b/src/addon/blog/providers/index-link-handler.ts index 189fad7a34c..0670b322b53 100644 --- a/src/addon/blog/providers/index-link-handler.ts +++ b/src/addon/blog/providers/index-link-handler.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; -import { CoreLoginHelperProvider } from '@core/login/providers/helper'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { AddonBlogProvider } from './blog'; /** @@ -27,7 +27,7 @@ export class AddonBlogIndexLinkHandler extends CoreContentLinksHandlerBase { featureName = 'CoreUserDelegate_AddonBlog:blogs'; pattern = /\/blog\/index\.php/; - constructor(private blogProvider: AddonBlogProvider, private loginHelper: CoreLoginHelperProvider) { + constructor(private blogProvider: AddonBlogProvider, private linkHelper: CoreContentLinksHelperProvider) { super(); } @@ -53,8 +53,7 @@ export class AddonBlogIndexLinkHandler extends CoreContentLinksHandlerBase { return [{ action: (siteId, navCtrl?): void => { - // Always use redirect to make it the new history root (to avoid "loops" in history). - this.loginHelper.redirect('AddonBlogEntriesPage', pageParams, siteId); + this.linkHelper.goInSite(navCtrl, 'AddonBlogEntriesPage', pageParams, siteId, !Object.keys(pageParams).length); } }]; } diff --git a/src/addon/blog/providers/user-handler.ts b/src/addon/blog/providers/user-handler.ts index 039b9ed56ef..fe19563b8c1 100644 --- a/src/addon/blog/providers/user-handler.ts +++ b/src/addon/blog/providers/user-handler.ts @@ -63,7 +63,7 @@ export class AddonBlogUserHandler implements CoreUserProfileHandler { action: (event, navCtrl, user, courseId): void => { event.preventDefault(); event.stopPropagation(); - // Always use redirect to make it the new history root (to avoid "loops" in history). + this.linkHelper.goInSite(navCtrl, 'AddonBlogEntriesPage', { userId: user.id, courseId: courseId }); } }; diff --git a/src/addon/competency/providers/plans-link-handler.ts b/src/addon/competency/providers/plans-link-handler.ts index a99280f3adc..da08f33dacd 100644 --- a/src/addon/competency/providers/plans-link-handler.ts +++ b/src/addon/competency/providers/plans-link-handler.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; -import { CoreLoginHelperProvider } from '@core/login/providers/helper'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { AddonCompetencyProvider } from './competency'; /** @@ -26,7 +26,7 @@ export class AddonCompetencyPlansLinkHandler extends CoreContentLinksHandlerBase name = 'AddonCompetencyPlansLinkHandler'; pattern = /\/admin\/tool\/lp\/plans\.php/; - constructor(private loginHelper: CoreLoginHelperProvider, private competencyProvider: AddonCompetencyProvider) { + constructor(private linkHelper: CoreContentLinksHelperProvider, private competencyProvider: AddonCompetencyProvider) { super(); } @@ -44,8 +44,8 @@ export class AddonCompetencyPlansLinkHandler extends CoreContentLinksHandlerBase return [{ action: (siteId, navCtrl?): void => { - // Always use redirect to make it the new history root (to avoid "loops" in history). - this.loginHelper.redirect('AddonCompetencyPlanListPage', { userId: params.userid }, siteId); + this.linkHelper.goInSite(navCtrl, 'AddonCompetencyPlanListPage', { userId: params.userid }, siteId, + typeof params.userid == 'undefined'); } }]; } diff --git a/src/addon/competency/providers/user-handler.ts b/src/addon/competency/providers/user-handler.ts index 3b04349f6cb..7cdbaadac11 100644 --- a/src/addon/competency/providers/user-handler.ts +++ b/src/addon/competency/providers/user-handler.ts @@ -111,7 +111,7 @@ export class AddonCompetencyUserHandler implements CoreUserProfileHandler { action: (event, navCtrl, user, courseId): void => { event.preventDefault(); event.stopPropagation(); - // Always use redirect to make it the new history root (to avoid "loops" in history). + this.linkHelper.goInSite(navCtrl, 'AddonCompetencyCourseCompetenciesPage', {courseId, userId: user.id}); } }; @@ -123,7 +123,7 @@ export class AddonCompetencyUserHandler implements CoreUserProfileHandler { action: (event, navCtrl, user, courseId): void => { event.preventDefault(); event.stopPropagation(); - // Always use redirect to make it the new history root (to avoid "loops" in history). + this.linkHelper.goInSite(navCtrl, 'AddonCompetencyPlanListPage', {userId: user.id}); } }; diff --git a/src/addon/messages/providers/contact-request-link-handler.ts b/src/addon/messages/providers/contact-request-link-handler.ts index 1c262f601e4..aebf2e8cd32 100644 --- a/src/addon/messages/providers/contact-request-link-handler.ts +++ b/src/addon/messages/providers/contact-request-link-handler.ts @@ -43,7 +43,6 @@ export class AddonMessagesContactRequestLinkHandler extends CoreContentLinksHand CoreContentLinksAction[] | Promise { return [{ action: (siteId, navCtrl?): void => { - // Always use redirect to make it the new history root (to avoid "loops" in history). this.linkHelper.goInSite(navCtrl, 'AddonMessagesContactsPage', {}, siteId); } }]; diff --git a/src/addon/messages/providers/discussion-link-handler.ts b/src/addon/messages/providers/discussion-link-handler.ts index 869e9461b2c..b916798c621 100644 --- a/src/addon/messages/providers/discussion-link-handler.ts +++ b/src/addon/messages/providers/discussion-link-handler.ts @@ -49,7 +49,6 @@ export class AddonMessagesDiscussionLinkHandler extends CoreContentLinksHandlerB const stateParams = { userId: parseInt(params.id || params.user2, 10) }; - // Always use redirect to make it the new history root (to avoid "loops" in history). this.linkHelper.goInSite(navCtrl, 'AddonMessagesDiscussionPage', stateParams, siteId); } }]; diff --git a/src/addon/messages/providers/index-link-handler.ts b/src/addon/messages/providers/index-link-handler.ts index 974c6ddeff2..400bd4eb1af 100644 --- a/src/addon/messages/providers/index-link-handler.ts +++ b/src/addon/messages/providers/index-link-handler.ts @@ -49,7 +49,6 @@ export class AddonMessagesIndexLinkHandler extends CoreContentLinksHandlerBase { pageName = 'AddonMessagesGroupConversationsPage'; } - // Always use redirect to make it the new history root (to avoid "loops" in history). this.linkHelper.goInSite(navCtrl, pageName, undefined, siteId); } }]; diff --git a/src/addon/messages/providers/user-send-message-handler.ts b/src/addon/messages/providers/user-send-message-handler.ts index a80b90adeef..8fd8982d9d1 100644 --- a/src/addon/messages/providers/user-send-message-handler.ts +++ b/src/addon/messages/providers/user-send-message-handler.ts @@ -76,7 +76,6 @@ export class AddonMessagesSendMessageUserHandler implements CoreUserProfileHandl showKeyboard: true, userId: user.id }; - // Always use redirect to make it the new history root (to avoid "loops" in history). this.linkHelper.goInSite(navCtrl, 'AddonMessagesDiscussionPage', pageParams); } }; diff --git a/src/addon/notes/providers/user-handler.ts b/src/addon/notes/providers/user-handler.ts index c7a37d061ac..cd9ce80152d 100644 --- a/src/addon/notes/providers/user-handler.ts +++ b/src/addon/notes/providers/user-handler.ts @@ -99,7 +99,7 @@ export class AddonNotesUserHandler implements CoreUserProfileHandler { action: (event, navCtrl, user, courseId): void => { event.preventDefault(); event.stopPropagation(); - // Always use redirect to make it the new history root (to avoid "loops" in history). + this.linkHelper.goInSite(navCtrl, 'AddonNotesListPage', { userId: user.id, courseId: courseId }); } }; diff --git a/src/core/contentlinks/classes/module-list-handler.ts b/src/core/contentlinks/classes/module-list-handler.ts index 5fa6c41a763..37e44e7d7a4 100644 --- a/src/core/contentlinks/classes/module-list-handler.ts +++ b/src/core/contentlinks/classes/module-list-handler.ts @@ -63,7 +63,6 @@ export class CoreContentLinksModuleListHandler extends CoreContentLinksHandlerBa title: this.title || this.translate.instant('addon.mod_' + this.modName + '.modulenameplural') }; - // Always use redirect to make it the new history root (to avoid "loops" in history). this.linkHelper.goInSite(navCtrl, 'CoreCourseListModTypePage', stateParams, siteId); } }]; diff --git a/src/core/contentlinks/providers/helper.ts b/src/core/contentlinks/providers/helper.ts index bfd8b47b298..6e9f77e913a 100644 --- a/src/core/contentlinks/providers/helper.ts +++ b/src/core/contentlinks/providers/helper.ts @@ -30,6 +30,7 @@ import { CoreConstants } from '@core/constants'; import { CoreConfigConstants } from '../../../configconstants'; import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins'; import { CoreSite } from '@classes/site'; +import { CoreMainMenuProvider } from '@core/mainmenu/providers/mainmenu'; /** * Service that provides some features regarding content links. @@ -42,7 +43,8 @@ export class CoreContentLinksHelperProvider { private contentLinksDelegate: CoreContentLinksDelegate, private appProvider: CoreAppProvider, private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private translate: TranslateService, private initDelegate: CoreInitDelegate, eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider, - private sitePluginsProvider: CoreSitePluginsProvider, private zone: NgZone, private utils: CoreUtilsProvider) { + private sitePluginsProvider: CoreSitePluginsProvider, private zone: NgZone, private utils: CoreUtilsProvider, + private mainMenuProvider: CoreMainMenuProvider) { this.logger = logger.getInstance('CoreContentLinksHelperProvider'); } @@ -103,9 +105,10 @@ export class CoreContentLinksHelperProvider { * @param {string} pageName Name of the page to go. * @param {any} [pageParams] Params to send to the page. * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [checkMenu] If true, check if the root page of a main menu tab. Only the page name will be checked. * @return {Promise} Promise resolved when done. */ - goInSite(navCtrl: NavController, pageName: string, pageParams: any, siteId?: string): Promise { + goInSite(navCtrl: NavController, pageName: string, pageParams: any, siteId?: string, checkMenu?: boolean): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); const deferred = this.utils.promiseDefer(); @@ -113,7 +116,23 @@ export class CoreContentLinksHelperProvider { // Execute the code in the Angular zone, so change detection doesn't stop working. this.zone.run(() => { if (navCtrl && siteId == this.sitesProvider.getCurrentSiteId()) { - navCtrl.push(pageName, pageParams).then(deferred.resolve, deferred.reject); + if (checkMenu) { + // Check if the page is in the main menu. + this.mainMenuProvider.isCurrentMainMenuHandler(pageName, pageParams).catch(() => { + return false; // Shouldn't happen. + }).then((isInMenu) => { + if (isInMenu) { + // Just select the tab. + this.loginHelper.loadPageInMainMenu(pageName, pageParams); + + deferred.resolve(); + } else { + navCtrl.push(pageName, pageParams).then(deferred.resolve, deferred.reject); + } + }); + } else { + navCtrl.push(pageName, pageParams).then(deferred.resolve, deferred.reject); + } } else { this.loginHelper.redirect(pageName, pageParams, siteId).then(deferred.resolve, deferred.reject); } diff --git a/src/core/course/classes/main-resource-component.ts b/src/core/course/classes/main-resource-component.ts index b2a153add07..e357d088857 100644 --- a/src/core/course/classes/main-resource-component.ts +++ b/src/core/course/classes/main-resource-component.ts @@ -245,7 +245,6 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, * @param {any} event Event. */ gotoBlog(event: any): void { - // Always use redirect to make it the new history root (to avoid "loops" in history). this.linkHelper.goInSite(this.navCtrl, 'AddonBlogEntriesPage', { cmId: this.module.id }); } diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts index 1c87d2aed72..0cc337fd444 100644 --- a/src/core/course/components/format/format.ts +++ b/src/core/course/components/format/format.ts @@ -76,6 +76,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { loaded: boolean; protected sectionStatusObserver; + protected selectTabObserver; protected lastCourseFormat: string; constructor(private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService, private injector: Injector, @@ -124,6 +125,28 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { }); } }, this.sitesProvider.getCurrentSiteId()); + + // Listen for select course tab events to select the right section if needed. + this.selectTabObserver = eventsProvider.on(CoreEventsProvider.SELECT_COURSE_TAB, (data) => { + + if (!data.name) { + let section; + + if (typeof data.sectionId != 'undefined' && data.sectionId != null && this.sections) { + section = this.sections.find((section) => { + return section.id == data.sectionId; + }); + } else if (typeof data.sectionNumber != 'undefined' && data.sectionNumber != null && this.sections) { + section = this.sections.find((section) => { + return section.section == data.sectionNumber; + }); + } + + if (section) { + this.sectionChanged(section); + } + } + }); } /** @@ -437,9 +460,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { * Component destroyed. */ ngOnDestroy(): void { - if (this.sectionStatusObserver) { - this.sectionStatusObserver.off(); - } + this.sectionStatusObserver && this.sectionStatusObserver.off(); + this.selectTabObserver && this.selectTabObserver.off(); } /** diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index c2d67c3f1e7..6ddb8b33bb2 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -67,6 +67,7 @@ export class CoreCourseSectionPage implements OnDestroy { protected modParams: any; protected completionObserver; protected courseStatusObserver; + protected selectTabObserver; protected syncObserver; protected firstTabName: string; protected isDestroyed = false; @@ -120,6 +121,26 @@ export class CoreCourseSectionPage implements OnDestroy { } }, sitesProvider.getCurrentSiteId()); } + + this.selectTabObserver = eventsProvider.on(CoreEventsProvider.SELECT_COURSE_TAB, (data) => { + + if (!data.name) { + // If needed, set sectionId and sectionNumber. They'll only be used if the content tabs hasn't been loaded yet. + this.sectionId = data.sectionId || this.sectionId; + this.sectionNumber = data.sectionNumber || this.sectionNumber; + + // Select course contents. + this.tabsComponent && this.tabsComponent.selectTab(0); + } else if (this.courseHandlers) { + const index = this.courseHandlers.findIndex((handler) => { + return handler.name == data.name; + }); + + if (index >= 0) { + this.tabsComponent && this.tabsComponent.selectTab(index + 1); + } + } + }); } /** @@ -483,9 +504,8 @@ export class CoreCourseSectionPage implements OnDestroy { */ ngOnDestroy(): void { this.isDestroyed = true; - if (this.completionObserver) { - this.completionObserver.off(); - } + this.completionObserver && this.completionObserver.off(); + this.selectTabObserver && this.selectTabObserver.off(); } /** diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index bb4a55c5a89..ae8ab44df4a 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -164,6 +164,23 @@ export class CoreCourseProvider { }); } + /** + * Check if the current view in a NavController is a certain course initial page. + * + * @param {NavController} navCtrl NavController. + * @param {number} courseId Course ID. + * @return {boolean} Whether the current view is a certain course. + */ + currentViewIsCourse(navCtrl: NavController, courseId: number): boolean { + if (navCtrl) { + const view = navCtrl.getActive(); + + return view && view.id == 'CoreCourseSectionPage' && view.data && view.data.course && view.data.course.id == courseId; + } + + return false; + } + /** * Get completion status of all the activities in a course for a certain user. * @@ -973,6 +990,19 @@ export class CoreCourseProvider { }); } + /** + * Select a certain tab in the course. Please use currentViewIsCourse() first to verify user is viewing the course. + * + * @param {string} [name] Name of the tab. If not provided, course contents. + * @param {any} [params] Other params. + */ + selectCourseTab(name?: string, params?: any): void { + params = params || {}; + params.name = name || ''; + + this.eventsProvider.trigger(CoreEventsProvider.SELECT_COURSE_TAB, params); + } + /** * Change the course status, setting it to the previous status. * diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index 04a9784238a..c458b999c47 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -119,15 +119,16 @@ export class CoreCourseHelperProvider { protected logger; constructor(private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider, - private moduleDelegate: CoreCourseModuleDelegate, private prefetchDelegate: CoreCourseModulePrefetchDelegate, - private filepoolProvider: CoreFilepoolProvider, private sitesProvider: CoreSitesProvider, - private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider, - private utils: CoreUtilsProvider, private translate: TranslateService, private loginHelper: CoreLoginHelperProvider, - private courseOptionsDelegate: CoreCourseOptionsDelegate, private siteHomeProvider: CoreSiteHomeProvider, - private eventsProvider: CoreEventsProvider, private fileHelper: CoreFileHelperProvider, - private appProvider: CoreAppProvider, private fileProvider: CoreFileProvider, private injector: Injector, - private coursesProvider: CoreCoursesProvider, private courseOffline: CoreCourseOfflineProvider, - private loggerProvider: CoreLoggerProvider) { + private moduleDelegate: CoreCourseModuleDelegate, private prefetchDelegate: CoreCourseModulePrefetchDelegate, + private filepoolProvider: CoreFilepoolProvider, private sitesProvider: CoreSitesProvider, + private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider, + private utils: CoreUtilsProvider, private translate: TranslateService, private loginHelper: CoreLoginHelperProvider, + private courseOptionsDelegate: CoreCourseOptionsDelegate, private siteHomeProvider: CoreSiteHomeProvider, + private eventsProvider: CoreEventsProvider, private fileHelper: CoreFileHelperProvider, + private appProvider: CoreAppProvider, private fileProvider: CoreFileProvider, private injector: Injector, + private coursesProvider: CoreCoursesProvider, private courseOffline: CoreCourseOfflineProvider, + loggerProvider: CoreLoggerProvider) { + this.logger = loggerProvider.getInstance('CoreCourseHelperProvider'); } @@ -1170,7 +1171,7 @@ export class CoreCourseHelperProvider { // Otherwise, we will redirect below. modal.dismiss(); - return module.handlerData.action(event, navCtrl, module, courseId); + return module.handlerData.action(new Event('click'), navCtrl, module, courseId); } this.logger.warn('navCtrl was not passed to navigateToModule by the link handler for ' + module.modname); diff --git a/src/core/courses/providers/course-link-handler.ts b/src/core/courses/providers/course-link-handler.ts index dc688e1c414..5ffe35cb68f 100644 --- a/src/core/courses/providers/course-link-handler.ts +++ b/src/core/courses/providers/course-link-handler.ts @@ -40,8 +40,9 @@ export class CoreCoursesCourseLinkHandler extends CoreContentLinksHandlerBase { private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private courseProvider: CoreCourseProvider, private textUtils: CoreTextUtilsProvider, private courseHelper: CoreCourseHelperProvider, - private loggerProvider: CoreLoggerProvider) { + loggerProvider: CoreLoggerProvider) { super(); + this.logger = loggerProvider.getInstance('CoreCoursesCourseLinkHandler'); } @@ -80,9 +81,17 @@ export class CoreCoursesCourseLinkHandler extends CoreContentLinksHandlerBase { action: (siteId, navCtrl?): void => { siteId = siteId || this.sitesProvider.getCurrentSiteId(); if (siteId == this.sitesProvider.getCurrentSiteId()) { - this.actionEnrol(courseId, url, pageParams, navCtrl).catch(() => { - // Ignore errors. - }); + // Check if we already are in the course index page. + if (this.courseProvider.currentViewIsCourse(navCtrl, courseId)) { + // Current view is this course, just select the contents tab. + this.courseProvider.selectCourseTab('', pageParams); + + return; + } else { + this.actionEnrol(courseId, url, pageParams, navCtrl).catch(() => { + // Ignore errors. + }); + } } else { // Don't pass the navCtrl to make the course the new history root (to avoid "loops" in history). this.courseHelper.getAndOpenCourse(undefined, courseId, pageParams, siteId); diff --git a/src/core/courses/providers/courses-index-link-handler.ts b/src/core/courses/providers/courses-index-link-handler.ts index 834629eacb2..bbea7ec34fa 100644 --- a/src/core/courses/providers/courses-index-link-handler.ts +++ b/src/core/courses/providers/courses-index-link-handler.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; -import { CoreLoginHelperProvider } from '@core/login/providers/helper'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreCoursesProvider } from './courses'; /** @@ -27,7 +27,7 @@ export class CoreCoursesIndexLinkHandler extends CoreContentLinksHandlerBase { featureName = 'CoreMainMenuDelegate_CoreCourses'; pattern = /\/course\/?(index\.php.*)?$/; - constructor(private coursesProvider: CoreCoursesProvider, private loginHelper: CoreLoginHelperProvider) { + constructor(private coursesProvider: CoreCoursesProvider, private linkHelper: CoreContentLinksHelperProvider) { super(); } @@ -56,8 +56,7 @@ export class CoreCoursesIndexLinkHandler extends CoreContentLinksHandlerBase { } } - // Always use redirect to make it the new history root (to avoid "loops" in history). - this.loginHelper.redirect(page, pageParams, siteId); + this.linkHelper.goInSite(navCtrl, page, pageParams, siteId); } }]; } diff --git a/src/core/courses/providers/dashboard-link-handler.ts b/src/core/courses/providers/dashboard-link-handler.ts index 26f00164ef7..aaa0d7ab511 100644 --- a/src/core/courses/providers/dashboard-link-handler.ts +++ b/src/core/courses/providers/dashboard-link-handler.ts @@ -43,7 +43,7 @@ export class CoreCoursesDashboardLinkHandler extends CoreContentLinksHandlerBase CoreContentLinksAction[] | Promise { return [{ action: (siteId, navCtrl?): void => { - // Always use redirect to make it the new history root (to avoid "loops" in history). + // Use redirect to select the tab. this.loginHelper.redirect('CoreCoursesDashboardPage', undefined, siteId); } }]; diff --git a/src/core/grades/providers/helper.ts b/src/core/grades/providers/helper.ts index 5df58c9dd54..22d925f6a16 100644 --- a/src/core/grades/providers/helper.ts +++ b/src/core/grades/providers/helper.ts @@ -24,7 +24,6 @@ import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; -import { CoreLoginHelperProvider } from '@core/login/providers/helper'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; /** @@ -38,8 +37,7 @@ export class CoreGradesHelperProvider { private gradesProvider: CoreGradesProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private utils: CoreUtilsProvider, - private linkHelper: CoreContentLinksHelperProvider, private loginHelper: CoreLoginHelperProvider, - private courseHelper: CoreCourseHelperProvider) { + private linkHelper: CoreContentLinksHelperProvider, private courseHelper: CoreCourseHelperProvider) { this.logger = logger.getInstance('CoreGradesHelperProvider'); } @@ -457,14 +455,22 @@ export class CoreGradesHelperProvider { }); } - // View own grades. Open the course with the grades tab selected. + // View own grades. Check if we already are in the course index page. + if (this.courseProvider.currentViewIsCourse(navCtrl, courseId)) { + // Current view is this course, just select the grades tab. + this.courseProvider.selectCourseTab('CoreGrades'); + + return; + } + + // Open the course with the grades tab selected. return this.courseHelper.getCourse(courseId, siteId).then((result) => { const pageParams: any = { course: result.course, selectedTab: 'CoreGrades' }; - return this.loginHelper.redirect('CoreCourseSectionPage', pageParams, siteId).catch(() => { + return this.linkHelper.goInSite(navCtrl, 'CoreCourseSectionPage', pageParams, siteId).catch(() => { // Ignore errors. }); }); diff --git a/src/core/grades/providers/overview-link-handler.ts b/src/core/grades/providers/overview-link-handler.ts index 32ab28ac111..1f34a82b016 100644 --- a/src/core/grades/providers/overview-link-handler.ts +++ b/src/core/grades/providers/overview-link-handler.ts @@ -43,7 +43,6 @@ export class CoreGradesOverviewLinkHandler extends CoreContentLinksHandlerBase { CoreContentLinksAction[] | Promise { return [{ action: (siteId, navCtrl?): void => { - // Always use redirect to make it the new history root (to avoid "loops" in history). this.linkHelper.goInSite(navCtrl, 'CoreGradesCoursesPage', undefined, siteId); } }]; diff --git a/src/core/grades/providers/user-handler.ts b/src/core/grades/providers/user-handler.ts index 1b8e61de9db..20d1d3a49a2 100644 --- a/src/core/grades/providers/user-handler.ts +++ b/src/core/grades/providers/user-handler.ts @@ -112,7 +112,6 @@ export class CoreGradesUserHandler implements CoreUserProfileHandler { courseId: courseId, userId: user.id }; - // Always use redirect to make it the new history root (to avoid "loops" in history). this.linkHelper.goInSite(navCtrl, 'CoreGradesCoursePage', pageParams); } }; diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index 2d4bbc62f61..dd54ecac62e 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -629,7 +629,7 @@ export class CoreLoginHelperProvider { * @param {string} page Name of the page to load. * @param {any} params Params to pass to the page. */ - protected loadPageInMainMenu(page: string, params: any): void { + loadPageInMainMenu(page: string, params: any): void { if (!this.appProvider.isMainMenuOpen()) { // Main menu not open. Store the page to be loaded later. this.pageToLoad = { diff --git a/src/core/mainmenu/providers/mainmenu.ts b/src/core/mainmenu/providers/mainmenu.ts index 5f8fe59ed9a..d601956c427 100644 --- a/src/core/mainmenu/providers/mainmenu.ts +++ b/src/core/mainmenu/providers/mainmenu.ts @@ -16,7 +16,9 @@ import { Injectable } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreLangProvider } from '@providers/lang'; import { CoreSitesProvider } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreConfigConstants } from '../../../configconstants'; +import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from './delegate'; /** * Custom main menu item. @@ -56,10 +58,34 @@ export class CoreMainMenuProvider { static ITEM_MIN_WIDTH = 72; // Min with of every item, based on 5 items on a 360 pixel wide screen. protected tablet = false; - constructor(private langProvider: CoreLangProvider, private sitesProvider: CoreSitesProvider) { + constructor(private langProvider: CoreLangProvider, private sitesProvider: CoreSitesProvider, + protected menuDelegate: CoreMainMenuDelegate, protected utils: CoreUtilsProvider) { this.tablet = window && window.innerWidth && window.innerWidth >= 576 && window.innerHeight >= 576; } + /** + * Get the current main menu handlers. + * + * @return {Promise} Promise resolved with the current main menu handlers. + */ + getCurrentMainMenuHandlers(): Promise { + const deferred = this.utils.promiseDefer(); + + const subscription = this.menuDelegate.getHandlers().subscribe((handlers) => { + subscription && subscription.unsubscribe(); + + // Remove the handlers that should only appear in the More menu. + handlers = handlers.filter((handler) => { + return !handler.onlyInMore; + }); + + // Return main handlers. + deferred.resolve(handlers.slice(0, this.getNumItems())); + }); + + return deferred.promise; + } + /** * Get a list of custom menu items for a certain site. * @@ -211,6 +237,23 @@ export class CoreMainMenuProvider { return tablet ? 'side' : 'bottom'; } + /** + * Check if a certain page is the root of a main menu handler currently displayed. + * + * @param {string} page Name of the page. + * @param {string} [pageParams] Page params. + * @return {Promise} Promise resolved with boolean: whether it's the root of a main menu handler. + */ + isCurrentMainMenuHandler(pageName: string, pageParams?: any): Promise { + return this.getCurrentMainMenuHandlers().then((handlers) => { + const handler = handlers.find((handler, i) => { + return handler.page == pageName; + }); + + return !!handler; + }); + } + /** * Check if responsive main menu items is disabled in the current site. * diff --git a/src/core/sitehome/providers/index-link-handler.ts b/src/core/sitehome/providers/index-link-handler.ts index eb2cb59060f..f0e15ad317a 100644 --- a/src/core/sitehome/providers/index-link-handler.ts +++ b/src/core/sitehome/providers/index-link-handler.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; -import { CoreLoginHelperProvider } from '@core/login/providers/helper'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreSiteHomeProvider } from './sitehome'; /** @@ -29,7 +29,7 @@ export class CoreSiteHomeIndexLinkHandler extends CoreContentLinksHandlerBase { pattern = /\/course\/view\.php.*([\?\&]id=\d+)/; constructor(private sitesProvider: CoreSitesProvider, private siteHomeProvider: CoreSiteHomeProvider, - private loginHelper: CoreLoginHelperProvider) { + private linkHelper: CoreContentLinksHelperProvider) { super(); } @@ -46,8 +46,7 @@ export class CoreSiteHomeIndexLinkHandler extends CoreContentLinksHandlerBase { CoreContentLinksAction[] | Promise { return [{ action: (siteId, navCtrl?): void => { - // Always use redirect to make it the new history root (to avoid "loops" in history). - this.loginHelper.redirect('CoreSiteHomeIndexPage', undefined, siteId); + this.linkHelper.goInSite(navCtrl, 'CoreSiteHomeIndexPage', undefined, siteId); } }]; } diff --git a/src/core/user/providers/participants-link-handler.ts b/src/core/user/providers/participants-link-handler.ts index b9bb3eddd8f..fb9971f02a5 100644 --- a/src/core/user/providers/participants-link-handler.ts +++ b/src/core/user/providers/participants-link-handler.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; -import { CoreLoginHelperProvider } from '@core/login/providers/helper'; +import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreUserProvider } from './user'; @@ -30,9 +30,9 @@ export class CoreUserParticipantsLinkHandler extends CoreContentLinksHandlerBase featureName = 'CoreCourseOptionsDelegate_CoreUserParticipants'; pattern = /\/user\/index\.php/; - constructor(private userProvider: CoreUserProvider, private loginHelper: CoreLoginHelperProvider, + constructor(private userProvider: CoreUserProvider, private courseHelper: CoreCourseHelperProvider, private domUtils: CoreDomUtilsProvider, - private linkHelper: CoreContentLinksHelperProvider) { + private linkHelper: CoreContentLinksHelperProvider, private courseProvider: CoreCourseProvider) { super(); } @@ -51,6 +51,14 @@ export class CoreUserParticipantsLinkHandler extends CoreContentLinksHandlerBase return [{ action: (siteId, navCtrl?): void => { + // Check if we already are in the course index page. + if (this.courseProvider.currentViewIsCourse(navCtrl, courseId)) { + // Current view is this course, just select the participants tab. + this.courseProvider.selectCourseTab('CoreUserParticipants'); + + return; + } + const modal = this.domUtils.showModalLoading(); this.courseHelper.getCourse(courseId, siteId).then((result) => { @@ -59,8 +67,9 @@ export class CoreUserParticipantsLinkHandler extends CoreContentLinksHandlerBase selectedTab: 'CoreUserParticipants' }; - // Always use redirect to make it the new history root (to avoid "loops" in history). - return this.loginHelper.redirect('CoreCourseSectionPage', params, siteId); + return this.linkHelper.goInSite(navCtrl, 'CoreCourseSectionPage', params, siteId).catch(() => { + // Ignore errors. + }); }).catch(() => { // Cannot get course for some reason, just open the participants page. return this.linkHelper.goInSite(navCtrl, 'CoreUserParticipantsPage', {courseId: courseId}, siteId); diff --git a/src/core/user/providers/user-link-handler.ts b/src/core/user/providers/user-link-handler.ts index faaa55c8185..eb9a0b0b65a 100644 --- a/src/core/user/providers/user-link-handler.ts +++ b/src/core/user/providers/user-link-handler.ts @@ -47,7 +47,6 @@ export class CoreUserProfileLinkHandler extends CoreContentLinksHandlerBase { courseId: params.course, userId: parseInt(params.id, 10) }; - // Always use redirect to make it the new history root (to avoid "loops" in history). this.linkHelper.goInSite(navCtrl, 'CoreUserProfilePage', pageParams, siteId); } }]; diff --git a/src/providers/events.ts b/src/providers/events.ts index 650cc55e833..fe75a177a43 100644 --- a/src/providers/events.ts +++ b/src/providers/events.ts @@ -60,6 +60,7 @@ export class CoreEventsProvider { static LOAD_PAGE_MAIN_MENU = 'load_page_main_menu'; static SEND_ON_ENTER_CHANGED = 'send_on_enter_changed'; static MAIN_MENU_OPEN = 'main_menu_open'; + static SELECT_COURSE_TAB = 'select_course_tab'; protected logger; protected observables: { [s: string]: Subject } = {}; From dc5b9875b36812ea457e8c1342620fe67b2cf391 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 18 Jun 2019 11:37:06 +0200 Subject: [PATCH 009/241] MOBILE-2735 tabs: Ask confirm when going to root of current tab --- scripts/langindex.json | 2 + src/assets/lang/en.json | 2 + src/components/ion-tabs/core-ion-tabs.html | 2 +- src/components/ion-tabs/ion-tabs.ts | 58 ++++++++++++++++++---- src/lang/en.json | 2 + 5 files changed, 56 insertions(+), 10 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 7381c6fc8b7..6f1b7f091cd 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1260,6 +1260,8 @@ "core.completion-alt-manual-y-override": "completion", "core.confirmcanceledit": "local_moodlemobileapp", "core.confirmdeletefile": "repository", + "core.confirmgotabroot": "local_moodlemobileapp", + "core.confirmgotabrootdefault": "local_moodlemobileapp", "core.confirmloss": "local_moodlemobileapp", "core.confirmopeninbrowser": "local_moodlemobileapp", "core.considereddigitalminor": "moodle", diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 7ed0d48128f..6069bde0bc7 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1260,6 +1260,8 @@ "core.completion-alt-manual-y-override": "Completed: {{$a.modname}} (set by {{$a.overrideuser}}). Select to mark as not complete.", "core.confirmcanceledit": "Are you sure you want to leave this page? All changes will be lost.", "core.confirmdeletefile": "Are you sure you want to delete this file?", + "core.confirmgotabroot": "Are you sure you want to go back to {{name}}?", + "core.confirmgotabrootdefault": "Are you sure you want to go to the initial page of the current tab?", "core.confirmloss": "Are you sure? All changes will be lost.", "core.confirmopeninbrowser": "Do you want to open it in a web browser?", "core.considereddigitalminor": "You are too young to create an account on this site.", diff --git a/src/components/ion-tabs/core-ion-tabs.html b/src/components/ion-tabs/core-ion-tabs.html index 123d340c1c4..5a4dda4b37b 100644 --- a/src/components/ion-tabs/core-ion-tabs.html +++ b/src/components/ion-tabs/core-ion-tabs.html @@ -1,5 +1,5 @@
- +
diff --git a/src/components/ion-tabs/ion-tabs.ts b/src/components/ion-tabs/ion-tabs.ts index deeb76a531b..914d97ab6aa 100644 --- a/src/components/ion-tabs/ion-tabs.ts +++ b/src/components/ion-tabs/ion-tabs.ts @@ -20,11 +20,14 @@ import { import { CoreIonTabComponent } from './ion-tab'; import { CoreUtilsProvider, PromiseDefer } from '@providers/utils/utils'; import { CoreAppProvider } from '@providers/app'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { TranslateService } from '@ngx-translate/core'; /** - * Equivalent to ion-tabs. It has 2 improvements: + * Equivalent to ion-tabs. It has several improvements: * - If a core-ion-tab is added or removed, it will be reflected in the tab bar in the right position. * - It supports a loaded input to tell when are the tabs ready. + * - When the user clicks the tab again to go to root, a confirm modal is shown. */ @Component({ selector: 'core-ion-tabs', @@ -73,7 +76,8 @@ export class CoreIonTabsComponent extends Tabs implements OnDestroy { constructor(protected utils: CoreUtilsProvider, protected appProvider: CoreAppProvider, @Optional() parent: NavController, @Optional() viewCtrl: ViewController, _app: App, config: Config, elementRef: ElementRef, _plt: Platform, - renderer: Renderer, _linker: DeepLinker, keyboard?: Keyboard) { + renderer: Renderer, _linker: DeepLinker, protected domUtils: CoreDomUtilsProvider, + protected translate: TranslateService, keyboard?: Keyboard) { super(parent, viewCtrl, _app, config, elementRef, _plt, renderer, _linker, keyboard); } @@ -272,13 +276,25 @@ export class CoreIonTabsComponent extends Tabs implements OnDestroy { * * @param {number|Tab} tabOrIndex Index, or the Tab instance, of the tab to select. * @param {NavOptions} Nav options. - * @param {boolean} [fromUrl=true] Whether to load from a URL. + * @param {boolean} [fromUrl] Whether to load from a URL. + * @param {boolean} [manualClick] Whether the user manually clicked the tab. * @return {Promise} Promise resolved when selected. */ - select(tabOrIndex: number | Tab, opts: NavOptions = {}, fromUrl: boolean = false): Promise { + select(tabOrIndex: number | Tab, opts: NavOptions = {}, fromUrl?: boolean, manualClick?: boolean): Promise { if (this.initialized) { // Tabs have been initialized, select the tab. + if (manualClick) { + // If we'll go to the root of the current tab, ask the user to confirm first. + const tab = typeof tabOrIndex == 'number' ? this.getByIndex(tabOrIndex) : tabOrIndex; + + return this.confirmGoToRoot(tab).then(() => { + return super.select(tabOrIndex, opts, fromUrl); + }, () => { + // User cancelled. + }); + } + return super.select(tabOrIndex, opts, fromUrl); } else { // Tabs not initialized yet. Mark it as "selectedIndex" input so it's treated when the tabs are initialized. @@ -305,11 +321,16 @@ export class CoreIonTabsComponent extends Tabs implements OnDestroy { if (this.initialized) { const tab = this.getByIndex(index); if (tab) { - return tab.goToRoot({animate: false, updateUrl: true, isNavRoot: true}).then(() => { - // Tab not previously selected. Select it after going to root. - if (!tab.isSelected) { - return this.select(tab, {animate: false, updateUrl: true, isNavRoot: true}); - } + return this.confirmGoToRoot(tab).then(() => { + // User confirmed, go to root. + return tab.goToRoot({animate: tab.isSelected, updateUrl: true, isNavRoot: true}).then(() => { + // Tab not previously selected. Select it after going to root. + if (!tab.isSelected) { + return this.select(tab, {animate: false, updateUrl: true, isNavRoot: true}); + } + }); + }, () => { + // User cancelled. }); } @@ -349,4 +370,23 @@ export class CoreIonTabsComponent extends Tabs implements OnDestroy { // Unregister the custom back button action for this page this.unregisterBackButtonAction && this.unregisterBackButtonAction(); } + + /** + * Confirm if the user wants to go to the root of the current tab. + * + * @param {Tab} tab Tab to go to root. + * @return {Promise} Promise resolved when confirmed. + */ + confirmGoToRoot(tab: Tab): Promise { + if (!tab || !tab.isSelected || (tab.getActive() && tab.getActive().isFirst())) { + // Tab not selected or is already at root, no need to confirm. + return Promise.resolve(); + } else { + if (tab.tabTitle) { + return this.domUtils.showConfirm(this.translate.instant('core.confirmgotabroot', {name: tab.tabTitle})); + } else { + return this.domUtils.showConfirm(this.translate.instant('core.confirmgotabrootdefault')); + } + } + } } diff --git a/src/lang/en.json b/src/lang/en.json index bf2f305e543..fe910283220 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -40,6 +40,8 @@ "completion-alt-manual-y-override": "Completed: {{$a.modname}} (set by {{$a.overrideuser}}). Select to mark as not complete.", "confirmcanceledit": "Are you sure you want to leave this page? All changes will be lost.", "confirmdeletefile": "Are you sure you want to delete this file?", + "confirmgotabroot": "Are you sure you want to go back to {{name}}?", + "confirmgotabrootdefault": "Are you sure you want to go to the initial page of the current tab?", "confirmloss": "Are you sure? All changes will be lost.", "confirmopeninbrowser": "Do you want to open it in a web browser?", "considereddigitalminor": "You are too young to create an account on this site.", From b302ea0d5aa436138fa4afe164e3ccc243d4444d Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 26 Jun 2019 08:58:48 +0200 Subject: [PATCH 010/241] MOBILE-2735 course: Log view when changing sections --- src/core/course/components/format/format.ts | 10 +++++++++- src/core/course/pages/section/section.ts | 5 ----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts index 0cc337fd444..f296406f5ab 100644 --- a/src/core/course/components/format/format.ts +++ b/src/core/course/components/format/format.ts @@ -82,7 +82,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { constructor(private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService, private injector: Injector, private courseHelper: CoreCourseHelperProvider, private domUtils: CoreDomUtilsProvider, eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider, private content: Content, - prefetchDelegate: CoreCourseModulePrefetchDelegate, private modalCtrl: ModalController) { + prefetchDelegate: CoreCourseModulePrefetchDelegate, private modalCtrl: ModalController, + private courseProvider: CoreCourseProvider) { this.selectOptions.title = translate.instant('core.course.sections'); this.completionChanged = new EventEmitter(); @@ -335,6 +336,13 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { } else { this.domUtils.scrollToTop(this.content, 0); } + + if (!previousValue || previousValue.id != newSection.id) { + // First load or section changed, add log in Moodle. + this.courseProvider.logView(this.course.id, newSection.section, undefined, this.course.fullname).catch(() => { + // Ignore errors. + }); + } } /** diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index 6ddb8b33bb2..efbd0d7483a 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -234,11 +234,6 @@ export class CoreCourseSectionPage implements OnDestroy { }).then((sections) => { let promise; - // Add log in Moodle. - this.courseProvider.logView(this.course.id, this.sectionNumber, undefined, this.course.fullname).catch(() => { - // Ignore errors. - }); - // Get the completion status. if (this.course.enablecompletion === false) { // Completion not enabled. From 167cb339ff76e04e7b91b9219ca743e2f94123b2 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Tue, 18 Jun 2019 16:18:38 +0200 Subject: [PATCH 011/241] MOBILE-3053 rte: Fix height not updated on Android --- src/components/rich-text-editor/rich-text-editor.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/rich-text-editor/rich-text-editor.ts b/src/components/rich-text-editor/rich-text-editor.ts index 59fdabe77f5..a1254bad969 100644 --- a/src/components/rich-text-editor/rich-text-editor.ts +++ b/src/components/rich-text-editor/rich-text-editor.ts @@ -136,7 +136,11 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy const deferred = this.utils.promiseDefer(); setTimeout(() => { - const contentVisibleHeight = this.domUtils.getContentHeight(this.content) - this.kbHeight; + let contentVisibleHeight = this.domUtils.getContentHeight(this.content); + if (!this.platform.is('android')) { + // In Android we ignore the keyboard height because it is not part of the web view. + contentVisibleHeight -= this.kbHeight; + } if (contentVisibleHeight <= 0) { deferred.resolve(0); @@ -149,7 +153,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy let height; if (this.platform.is('android')) { - // Android, ignore keyboard height because web view is resized. + // In Android we ignore the keyboard height because it is not part of the web view. height = this.domUtils.getContentHeight(this.content) - this.getSurroundingHeight(this.element); } else if (this.platform.is('ios') && this.kbHeight > 0) { // Keyboard open in iOS. From 51086fa8480f1e6ba5d65e5d84941c02e8458b20 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 19 Jun 2019 15:31:21 +0200 Subject: [PATCH 012/241] MOBILE-3053 rte: Scrollable toolbar --- .../core-rich-text-editor.html | 108 +++++++++++---- .../rich-text-editor/rich-text-editor.scss | 90 +++++++----- .../rich-text-editor/rich-text-editor.ts | 130 ++++++++++++++++-- 3 files changed, 257 insertions(+), 71 deletions(-) diff --git a/src/components/rich-text-editor/core-rich-text-editor.html b/src/components/rich-text-editor/core-rich-text-editor.html index 7a02610ddf7..40e06d3f4af 100644 --- a/src/components/rich-text-editor/core-rich-text-editor.html +++ b/src/components/rich-text-editor/core-rich-text-editor.html @@ -1,33 +1,81 @@ -
-
-
- - -
-
- - - - - - - - - - - - -
-
-
- -
- -
-
- -
-
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/components/rich-text-editor/rich-text-editor.scss b/src/components/rich-text-editor/rich-text-editor.scss index 8bce984e8e2..0cf3a59606d 100644 --- a/src/components/rich-text-editor/rich-text-editor.scss +++ b/src/components/rich-text-editor/rich-text-editor.scss @@ -4,27 +4,20 @@ ion-app.app-root core-rich-text-editor { min-height: 200px; /* Just in case vh is not supported */ min-height: 40vh; width: 100%; - position: relative; - display: block; + display: flex; + flex-direction: column; - > div { - position: absolute; - @include position(0, 0, 0, 0); - height: 100%; - width: 100%; - display: flex; - flex-direction: column; - } .core-rte-editor, .core-textarea { padding: 2px; margin: 2px; width: 100%; resize: none; background-color: $white; - flex-grow: 1; } .core-rte-editor { + flex-grow: 1; + flex-shrink: 1; -webkit-user-select: auto !important; word-wrap: break-word; overflow-x: hidden; @@ -48,6 +41,8 @@ ion-app.app-root core-rich-text-editor { } .core-textarea { + flex-grow: 1; + flex-shrink: 1; position: relative; textarea { @@ -64,34 +59,65 @@ ion-app.app-root core-rich-text-editor { } div.core-rte-toolbar { - background: $gray-darker; - @include margin(0px, 1px, 15px, 1px); - text-align: center; - flex-grow: 0; + display: flex; width: 100%; z-index: 1; + flex-grow: 0; + flex-shrink: 0; + background-color: $white; + @include padding(5px, null); + border-top: 1px solid $gray; - .core-rte-buttons { + ion-slides { + width: 240px; + flex-grow: 1; + flex-shrink: 1; + } + + button { display: flex; + justify-content: center; align-items: center; - flex-direction: row; - flex-wrap: wrap; - justify-content: space-evenly; - - button { - background: $gray-darker; - color: $white; - font-size: 1.1em; - height: 35px; - min-width: 30px; - @include padding(null, 3px, null, 3px); - @include border-end(qpx, solid, $gray-dark); - border-bottom: 1px solid $gray-dark; - @include position(-6px, 0, null, null); - flex-grow: 1; - margin: 0; + width: 36px; + height: 36px; + margin: 0 auto; + font-size: 18px; + background-color: $white; + border-radius: 4px; + @include core-transition(background-color, 200ms); + color: $text-color; + cursor: pointer; + + &.toolbar-button-enable { + width: 100%; + } + + &:active, &[aria-pressed="true"] { + background-color: $gray; + } + + &.toolbar-arrow { + width: 28px; + flex-grow: 0; + flex-shrink: 0; + opacity: 1; + @include core-transition(opacity, 200ms); + + &:active { + background-color: $white; + } + + &.toolbar-arrow-hidden { + opacity: 0; + } } } + + &.toolbar-hidden { + visibility: none; + height: 0; + border: none; + } } } diff --git a/src/components/rich-text-editor/rich-text-editor.ts b/src/components/rich-text-editor/rich-text-editor.ts index a1254bad969..f66f553f156 100644 --- a/src/components/rich-text-editor/rich-text-editor.ts +++ b/src/components/rich-text-editor/rich-text-editor.ts @@ -14,7 +14,7 @@ import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterContentInit, OnDestroy, Optional } from '@angular/core'; -import { TextInput, Content, Platform } from 'ionic-angular'; +import { TextInput, Content, Platform, Slides } from 'ionic-angular'; import { CoreSitesProvider } from '@providers/sites'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; @@ -56,7 +56,6 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy @ViewChild('editor') editor: ElementRef; // WYSIWYG editor. @ViewChild('textarea') textarea: TextInput; // Textarea editor. - @ViewChild('decorate') decorate: ElementRef; // Buttons. protected element: HTMLDivElement; protected editorElement: HTMLDivElement; @@ -71,6 +70,20 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy rteEnabled = false; editorSupported = true; + // Toolbar. + @ViewChild('toolbar') toolbar: ElementRef; + @ViewChild(Slides) toolbarSlides: Slides; + isPhone = this.platform.is('mobile') && !this.platform.is('tablet'); + toolbarHidden = this.isPhone; + numToolbarButtons = 6; + toolbarArrows = false; + toolbarPrevHidden = true; + toolbarNextHidden = false; + + protected isCurrentView = true; + protected toolbarButtonWidth = 40; + protected toolbarArrowWidth = 28; + constructor(private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private sitesProvider: CoreSitesProvider, private filepoolProvider: CoreFilepoolProvider, @Optional() private content: Content, elementRef: ElementRef, private events: CoreEventsProvider, @@ -123,6 +136,8 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy this.kbHeight = kbHeight; this.maximizeEditorSize(); }); + + this.updateToolbarButtons(); } /** @@ -390,13 +405,16 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy this.rteEnabled = !this.rteEnabled; // Set focus and cursor at the end. - setTimeout(() => { - if (this.rteEnabled) { - this.editorElement.focus(); - } else { - this.textarea.setFocus(); - } - }); + // Modify the DOM directly so the keyboard stays open. + if (this.rteEnabled) { + this.editorElement.removeAttribute('hidden'); + this.textarea.getNativeElement().setAttribute('hidden', ''); + this.editorElement.focus(); + } else { + this.editorElement.setAttribute('hidden', ''); + this.textarea.getNativeElement().removeAttribute('hidden'); + this.textarea.setFocus(); + } } /** @@ -508,6 +526,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy protected buttonAction($event: any, command: string): void { $event.preventDefault(); $event.stopPropagation(); + this.editorElement.focus(); if (command) { if (command.includes('|')) { @@ -521,6 +540,99 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy } } + /** + * Hide the toolbar. + */ + hideToolbar(): void { + this.editorElement.focus(); + this.toolbarHidden = true; + } + + /** + * Show the toolbar. + */ + showToolbar(): void { + this.editorElement.focus(); + this.toolbarHidden = false; + } + + /** + * Method that shows the next toolbar buttons. + */ + toolbarNext(): void { + if (!this.toolbarNextHidden) { + const currentIndex = this.toolbarSlides.getActiveIndex() || 0; + this.toolbarSlides.slideTo(currentIndex + this.numToolbarButtons); + } + this.editorElement.focus(); + } + + /** + * Method that shows the previous toolbar buttons. + */ + toolbarPrev(): void { + if (!this.toolbarPrevHidden) { + const currentIndex = this.toolbarSlides.getActiveIndex() || 0; + this.toolbarSlides.slideTo(currentIndex - this.numToolbarButtons); + } + this.editorElement.focus(); + } + + /** + * Update the number of toolbar buttons displayed. + */ + updateToolbarButtons(): void { + if (!this.isCurrentView) { + // Don't calculate if component isn't in current view, the calculations are wrong. + return; + } + + if (!(this.toolbarSlides as any)._init) { + // Slides is not initialized yet, try later. + setTimeout(this.updateToolbarButtons.bind(this), 100); + + return; + } + + const width = this.domUtils.getElementWidth(this.toolbar.nativeElement); + if (width > this.toolbarSlides.length() * this.toolbarButtonWidth) { + this.numToolbarButtons = this.toolbarSlides.length(); + this.toolbarArrows = false; + } else { + this.numToolbarButtons = Math.floor((width - this.toolbarArrowWidth * 2) / this.toolbarButtonWidth); + this.toolbarArrows = true; + } + + this.toolbarSlides.update(); + + this.updateToolbarArrows(); + } + + /** + * Show or hide next/previous toolbar arrows. + */ + updateToolbarArrows(): void { + const currentIndex = this.toolbarSlides.getActiveIndex() || 0; + this.toolbarPrevHidden = currentIndex <= 0; + this.toolbarNextHidden = currentIndex + this.numToolbarButtons >= this.toolbarSlides.length(); + } + + /** + * User entered the page that contains the component. + */ + ionViewDidEnter(): void { + this.isCurrentView = true; + + this.updateToolbarButtons(); + } + + /** + * User left the page that contains the component. + */ + ionViewDidLeave(): void { + this.isCurrentView = false; + } + /** * Component being destroyed. */ From 0943b0c43118da5af1b28c785a5650c23e4f3edd Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 19 Jun 2019 15:37:03 +0200 Subject: [PATCH 013/241] MOBILE-3053 rte: Highlight toolbar styles --- .../core-rich-text-editor.html | 20 +++---- .../rich-text-editor/rich-text-editor.ts | 52 ++++++++++++++++--- 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/src/components/rich-text-editor/core-rich-text-editor.html b/src/components/rich-text-editor/core-rich-text-editor.html index 40e06d3f4af..7874c0162ea 100644 --- a/src/components/rich-text-editor/core-rich-text-editor.html +++ b/src/components/rich-text-editor/core-rich-text-editor.html @@ -10,52 +10,52 @@ - - - - - - - - - - diff --git a/src/components/rich-text-editor/rich-text-editor.ts b/src/components/rich-text-editor/rich-text-editor.ts index f66f553f156..45e572d37fc 100644 --- a/src/components/rich-text-editor/rich-text-editor.ts +++ b/src/components/rich-text-editor/rich-text-editor.ts @@ -59,7 +59,6 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy protected element: HTMLDivElement; protected editorElement: HTMLDivElement; - protected resizeFunction; protected kbHeight = 0; // Last known keyboard height. protected minHeight = 200; // Minimum height of the editor. @@ -79,7 +78,18 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy toolbarArrows = false; toolbarPrevHidden = true; toolbarNextHidden = false; - + toolbarStyles = { + b: 'false', + i: 'false', + u: 'false', + strike: 'false', + p: 'false', + h1: 'false', + h2: 'false', + h3: 'false', + ul: 'false', + ol: 'false', + }; protected isCurrentView = true; protected toolbarButtonWidth = 40; protected toolbarArrowWidth = 28; @@ -119,8 +129,8 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy // Use paragraph on enter. document.execCommand('DefaultParagraphSeparator', false, 'p'); - this.resizeFunction = this.maximizeEditorSize.bind(this); - window.addEventListener('resize', this.resizeFunction); + window.addEventListener('resize', this.maximizeEditorSize); + document.addEventListener('selectionchange', this.updateToolbarStyles); let i = 0; this.initHeightInterval = setInterval(() => { @@ -145,7 +155,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy * * @return {Promise} Resolved with calculated editor size. */ - protected maximizeEditorSize(): Promise { + protected maximizeEditorSize = (): Promise => { this.content.resize(); const deferred = this.utils.promiseDefer(); @@ -617,6 +627,35 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy this.toolbarNextHidden = currentIndex + this.numToolbarButtons >= this.toolbarSlides.length(); } + /** + * Update highlighted toolbar styles. + */ + updateToolbarStyles = (): void => { + const node = document.getSelection().focusNode; + if (!node) { + return; + } + + let element = node.nodeType == 1 ? node as HTMLElement : node.parentElement; + const styles = {}; + + while (element != null && element !== this.editorElement) { + const tagName = element.tagName.toLowerCase(); + if (this.toolbarStyles[tagName]) { + styles[tagName] = 'true'; + } + element = element.parentElement; + } + + for (const tagName in this.toolbarStyles) { + this.toolbarStyles[tagName] = 'false'; + } + + if (element === this.editorElement) { + Object.assign(this.toolbarStyles, styles); + } + } + /** * User entered the page that contains the component. */ @@ -638,7 +677,8 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy */ ngOnDestroy(): void { this.valueChangeSubscription && this.valueChangeSubscription.unsubscribe(); - window.removeEventListener('resize', this.resizeFunction); + window.removeEventListener('resize', this.maximizeEditorSize); + document.removeEventListener('selectionchange', this.updateToolbarStyles); clearInterval(this.initHeightInterval); this.keyboardObs && this.keyboardObs.off(); } From 627ae616b4c168f8619ad28b27015062117b255a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 14 Jun 2019 12:09:02 +0200 Subject: [PATCH 014/241] MOBILE-3077 travis: Remove desktop build scripts --- package.json | 4 ++-- scripts/aot.sh | 31 ++++++++++++++++++++++++------- scripts/linux.sh | 38 -------------------------------------- 3 files changed, 26 insertions(+), 47 deletions(-) delete mode 100755 scripts/linux.sh diff --git a/package.json b/package.json index 7ad1f91ae4d..fc78bb983e8 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "postionic:build": "gulp copy-component-templates", "desktop.pack": "electron-builder --dir", "desktop.dist": "electron-builder -p never", - "windows.store": "electron-windows-store --input-directory .\\desktop\\dist\\win-unpacked --output-directory .\\desktop\\store --flatten true -a .\\resources\\desktop -m .\\desktop\\assets\\windows\\AppXManifest.xml --package-version 0.0.0.0 --package-name MoodleDesktop" + "windows.store": "electron-windows-store --input-directory .\\desktop\\dist\\win-unpacked --output-directory .\\desktop\\store -a .\\resources\\desktop -m .\\desktop\\assets\\windows\\AppXManifest.xml --package-version 0.0.0.0 --package-name MoodleDesktop" }, "dependencies": { "@angular/animations": "^5.2.10", @@ -210,7 +210,7 @@ } ], "compression": "maximum", - "electronVersion": "4.0.1", + "electronVersion": "5.0.4", "mac": { "category": "public.app-category.education", "icon": "resources/desktop/icon.icns", diff --git a/scripts/aot.sh b/scripts/aot.sh index 5fa706e6472..4a2d344e26a 100755 --- a/scripts/aot.sh +++ b/scripts/aot.sh @@ -40,17 +40,34 @@ if [ ! -z $GIT_ORG ] && [ ! -z $GIT_TOKEN ] ; then gitfolder=${PWD##*/} git clone --depth 1 --no-single-branch https://github.com/$GIT_ORG/moodlemobile-phonegapbuild.git ../pgb pushd ../pgb + + mkdir /tmp/travistemp + cp .travis.yml /tmp/travistemp + mkdir /tmp/travistemp/scripts + cp scripts/* /tmp/travistemp/scripts + git checkout $TRAVIS_BRANCH - rm -Rf assets build index.html templates - cp -Rf ../$gitfolder/www/* ./ - rm -Rf assets/countries assets/mimetypes + + rm -Rf assets build index.html templates www destkop + + if [ $TRAVIS_BRANCH == 'desktop' ] ; then + cp -Rf ../$gitfolder/desktop ./ + cp -Rf ../$gitfolder/package.json ./ + cp -Rf ../$gitfolder/www ./ + rm -Rf www/assets/countries www/assets/mimetypes + else + cp -Rf ../$gitfolder/www/* ./ + rm -Rf assets/countries assets/mimetypes + fi + + cp /tmp/travistemp/.travis.yml .travis.yml + mkdir scripts + cp /tmp/travistemp/scripts/* scripts + + git add . git commit -m "Travis build: $TRAVIS_BUILD_NUMBER" git push https://$GIT_TOKEN@github.com/$GIT_ORG/moodlemobile-phonegapbuild.git popd fi -if [ ! -z $GIT_ORG_PRIVATE ] && [ ! -z $GIT_TOKEN ] && [ $TRAVIS_BRANCH == 'desktop' ] && [ $TRAVIS_OS_NAME == 'linux' ]; then - ./scripts/linux.sh -fi - diff --git a/scripts/linux.sh b/scripts/linux.sh deleted file mode 100755 index 571102f5b59..00000000000 --- a/scripts/linux.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -# -# Script for generating the Desktop builds -# - -sudo apt-get install -y libnss3-dev - -npm install -g electron-builder electron - -electron-builder install-app-deps - -jq -s '.[0] + {"name": "moodledesktop"}' package.json > package_new.json -mv package_new.json package.json - -rm -Rf desktop/dist - -npm run desktop.dist -- -l --x64 --ia32 - -if [ ! -z $GIT_ORG_PRIVATE ] && [ ! -z $GIT_TOKEN ] ; then - git clone -q https://$GIT_TOKEN@github.com/moodlemobile/bma-apps-builds.git ../apps - - mv desktop/dist/*.AppImage ../apps - - cd ../apps - - chmod +x *.AppImage - mv *i386.AppImage linux-ia32.AppImage - mv Moodle*.AppImage linux-x64.AppImage - ls - - tar -czvf MoodleDesktop32.tar.gz linux-ia32.AppImage - tar -czvf MoodleDesktop64.tar.gz linux-x64.AppImage - rm *.AppImage - - git add . - git commit -m "Linux desktop versions from Travis build $TRAVIS_BUILD_NUMBER" - git push -fi From fb84053da1b4765b41970711c2b770f146b081f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 28 Jun 2019 12:28:40 +0200 Subject: [PATCH 015/241] MOBILE-1332 lang: Improve language importing --- scripts/lang_functions.php | 426 +++++++++++++++++++++++++++++++++++++ scripts/moodle_to_json.php | 352 +----------------------------- 2 files changed, 435 insertions(+), 343 deletions(-) create mode 100644 scripts/lang_functions.php diff --git a/scripts/lang_functions.php b/scripts/lang_functions.php new file mode 100644 index 00000000000..56158a469d2 --- /dev/null +++ b/scripts/lang_functions.php @@ -0,0 +1,426 @@ +. + +/** + * Helper functions converting moodle strings to json. + */ + +function detect_languages($languages, $keys) { + echo "\n\n\n"; + + $all_languages = glob(LANGPACKSFOLDER.'/*' , GLOB_ONLYDIR); + function get_lang_from_dir($dir) { + return str_replace('_', '-', explode('/', $dir)[3]); + } + function get_lang_not_wp($langname) { + return (substr($langname, -3) !== '-wp'); + } + $all_languages = array_map('get_lang_from_dir', $all_languages); + $all_languages = array_filter($all_languages, 'get_lang_not_wp'); + + $detect_lang = array_diff($all_languages, $languages); + $new_langs = array(); + foreach ($detect_lang as $lang) { + reset_translations_strings(); + $new = detect_lang($lang, $keys); + if ($new) { + $new_langs[$lang] = $lang; + } + } + + return $new_langs; +} + +function build_languages($languages, $keys, $added_langs = []) { + // Process the languages. + foreach ($languages as $lang) { + reset_translations_strings(); + $ok = build_lang($lang, $keys); + if ($ok) { + $added_langs[$lang] = $lang; + } + } + + return $added_langs; +} + +function get_langindex_keys() { + // Process the index file, just once. + $keys = file_get_contents('langindex.json'); + $keys = (array) json_decode($keys); + + foreach ($keys as $key => $value) { + $map = new StdClass(); + if ($value == 'local_moodlemobileapp') { + $map->file = $value; + $map->string = $key; + } else { + $exp = explode('/', $value, 2); + $map->file = $exp[0]; + if (count($exp) == 2) { + $map->string = $exp[1]; + } else { + $exp = explode('.', $key, 3); + + if (count($exp) == 3) { + $map->string = $exp[2]; + } else { + $map->string = $exp[1]; + } + } + } + + $keys[$key] = $map; + } + + $total = count($keys); + echo "Total strings to translate $total\n"; + + return $keys; +} + +function add_langs_to_config($langs, $config) { + $changed = false; + $config_langs = get_object_vars($config['languages']); + foreach ($langs as $lang) { + if (!isset($config_langs[$lang])) { + $langfoldername = str_replace('-', '_', $lang); + + $string = []; + include(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php'); + $config['languages']->$lang = $string['thislanguage']; + $changed = true; + } + } + + if ($changed) { + // Sort languages by key. + $config['languages'] = json_decode( json_encode( $config['languages'] ), true ); + ksort($config['languages']); + $config['languages'] = json_decode( json_encode( $config['languages'] ), false ); + file_put_contents(CONFIG, json_encode($config, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); + } +} + +function get_langfolder($lang) { + $folder = LANGPACKSFOLDER.'/'.str_replace('-', '_', $lang); + if (!is_dir($folder) || !is_file($folder.'/langconfig.php')) { + echo "Cannot translate $folder, folder not found"; + + return false; + } + + return $folder; +} + +function get_translation_strings($langfoldername, $file, $override_folder = false) { + global $strings; + + if (isset($strings[$file])) { + return $strings[$file]; + } + + $string = import_translation_strings($langfoldername, $file); + $string_override = $override_folder ? import_translation_strings($override_folder, $file) : false; + + if ($string) { + $strings[$file] = $string; + if ($string_override) { + $strings[$file] = array_merge($strings[$file], $string_override); + } + } else if ($string_override) { + $strings[$file] = $string_override; + } else { + $strings[$file] = false; + } + + return $strings[$file]; +} + +function import_translation_strings($langfoldername, $file) { + $path = $langfoldername.'/'.$file.'.php'; + // Apply translations. + if (!file_exists($path)) { + return false; + } + + $string = []; + include($path); + + return $string; +} + +function reset_translations_strings() { + global $strings; + $strings = []; +} + +function build_lang($lang, $keys) { + $langfoldername = get_langfolder($lang); + if (!$langfoldername) { + return false; + } + + if (OVERRIDE_LANG_SUFIX) { + $override_langfolder = get_langfolder($lang.OVERRIDE_LANG_SUFIX); + } else { + $override_langfolder = false; + } + + $total = count ($keys); + $local = 0; + + $string = get_translation_strings($langfoldername, 'langconfig'); + $parent = isset($string['parentlanguage']) ? $string['parentlanguage'] : ""; + + echo "Processing $lang"; + if ($parent != "" && $parent != $lang) { + echo " ($parent)"; + } + + $langFile = false; + // Not yet translated. Do not override. + if (file_exists(ASSETSPATH.$lang.'.json')) { + // Load lang files just once. + $langFile = file_get_contents(ASSETSPATH.$lang.'.json'); + $langFile = (array) json_decode($langFile); + } + + $translations = []; + // Add the translation to the array. + foreach ($keys as $key => $value) { + $string = get_translation_strings($langfoldername, $value->file, $override_langfolder); + // Apply translations. + if (!$string) { + if (TOTRANSLATE) { + echo "\n\t\To translate $value->string on $value->file"; + } + continue; + } + + if (!isset($string[$value->string]) || ($lang == 'en' && $value->file == 'local_moodlemobileapp')) { + // Not yet translated. Do not override. + if ($langFile && is_array($langFile) && isset($langFile[$key])) { + $translations[$key] = $langFile[$key]; + $local++; + } + if (TOTRANSLATE) { + echo "\n\t\tTo translate $value->string on $value->file"; + } + continue; + } else { + $text = $string[$value->string]; + } + + if ($value->file != 'local_moodlemobileapp') { + $text = str_replace('$a->', '$a.', $text); + $text = str_replace('{$a', '{{$a', $text); + $text = str_replace('}', '}}', $text); + // Prevent double. + $text = str_replace(array('{{{', '}}}'), array('{{', '}}'), $text); + } else { + $local++; + } + + $translations[$key] = html_entity_decode($text); + } + + // Sort and save. + ksort($translations); + file_put_contents(ASSETSPATH.$lang.'.json', str_replace('\/', '/', json_encode($translations, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT))); + + $success = count($translations); + $percentage = floor($success/$total * 100); + echo "\t\t$success of $total -> $percentage% ($local local)\n"; + + if ($lang == 'en') { + generate_local_moodlemobileapp($keys, $translations); + override_component_lang_files($keys, $translations); + } + + return true; +} + +function detect_lang($lang, $keys) { + $langfoldername = get_langfolder($lang); + if (!$langfoldername) { + return false; + } + + $total = count ($keys); + $success = 0; + $local = 0; + + $string = get_translation_strings($langfoldername, 'langconfig'); + $parent = isset($string['parentlanguage']) ? $string['parentlanguage'] : ""; + if (!isset($string['thislanguage'])) { + echo "Cannot translate $lang, translated name not found"; + return false; + } + + echo "Checking $lang"; + if ($parent != "" && $parent != $lang) { + echo " ($parent)"; + } + $langname = $string['thislanguage']; + echo " ".$langname." -D"; + + // Add the translation to the array. + foreach ($keys as $key => $value) { + $string = get_translation_strings($langfoldername, $value->file); + // Apply translations. + if (!$string) { + continue; + } + + if (!isset($string[$value->string])) { + continue; + } else { + $text = $string[$value->string]; + } + + if ($value->file == 'local_moodlemobileapp') { + $local++; + } + + $success++; + } + + $percentage = floor($success/$total * 100); + echo "\t\t$success of $total -> $percentage% ($local local)"; + if (($percentage > 75 && $local > 50) || ($percentage > 50 && $local > 75)) { + echo " \t DETECTED\n"; + return true; + } + echo "\n"; + + return false; +} + +function save_key($key, $value, $path) { + $filePath = $path . '/en.json'; + + $file = file_get_contents($filePath); + $file = (array) json_decode($file); + $value = html_entity_decode($value); + if ($file[$key] != $value) { + $file[$key] = $value; + ksort($file); + file_put_contents($filePath, str_replace('\/', '/', json_encode($file, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT))); + } +} + +function override_component_lang_files($keys, $translations) { + echo "Override component lang files.\n"; + foreach ($translations as $key => $value) { + $path = '../src/'; + $exp = explode('.', $key, 3); + + $type = $exp[0]; + if (count($exp) == 3) { + $component = $exp[1]; + $plainid = $exp[2]; + } else { + $component = 'moodle'; + $plainid = $exp[1]; + } + switch($type) { + case 'core': + case 'addon': + switch($component) { + case 'moodle': + $path .= 'lang'; + break; + default: + $path .= $type.'/'.str_replace('_', '/', $component).'/lang'; + break; + } + break; + case 'assets': + $path .= $type.'/'.$component; + break; + + } + + if (is_file($path.'/en.json')) { + save_key($plainid, $value, $path); + } + } +} + +/** + * Generates local moodle mobile app file to update languages in AMOS. + * + * @param [array] $keys Translation keys. + * @param [array] $translations English translations. + */ +function generate_local_moodlemobileapp($keys, $translations) { + echo "Generate local_moodlemobileapp.\n"; + $string = '. + +/** + * Version details. + * + * @package local + * @subpackage moodlemobileapp + * @copyright 2014 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string[\'appstoredescription\'] = \'NOTE: This official Moodle Mobile app will ONLY work with Moodle sites that have been set up to allow it. Please talk to your Moodle administrator if you have any problems connecting. + +If your Moodle site has been configured correctly, you can use this app to: + +- browse the content of your courses, even when offline +- receive instant notifications of messages and other events +- quickly find and contact other people in your courses +- upload images, audio, videos and other files from your mobile device +- view your course grades +- and more! + +Please see http://docs.moodle.org/en/Mobile_app for all the latest information. + +We’d really appreciate any good reviews about the functionality so far, and your suggestions on what else you want this app to do! + +The app requires the following permissions: +Record audio - For recording audio to upload to Moodle +Read and modify the contents of your SD card - Contents are downloaded to the SD Card so you can see them offline +Network access - To be able to connect with your Moodle site and check if you are connected or not to switch to offline mode +Run at startup - So you receive local notifications even when the app is running in the background +Prevent phone from sleeping - So you can receive push notifications anytime\';'."\n"; + foreach ($keys as $key => $value) { + if (isset($translations[$key]) && $value->file == 'local_moodlemobileapp') { + $string .= '$string[\''.$key.'\'] = \''.str_replace("'", "\'", $translations[$key]).'\';'."\n"; + } + } + $string .= '$string[\'pluginname\'] = \'Moodle Mobile language strings\';'."\n"; + + file_put_contents('../../moodle-local_moodlemobileapp/lang/en/local_moodlemobileapp.php', $string."\n"); +} diff --git a/scripts/moodle_to_json.php b/scripts/moodle_to_json.php index bc6d128e544..e82eb95236b 100644 --- a/scripts/moodle_to_json.php +++ b/scripts/moodle_to_json.php @@ -26,6 +26,10 @@ define('LANGPACKSFOLDER', '../../moodle-langpacks'); define('ASSETSPATH', '../src/assets/lang/'); define('CONFIG', '../src/config.json'); +define('OVERRIDE_LANG_SUFIX', false); + +global $strings; +require_once('lang_functions.php'); $config = file_get_contents(CONFIG); $config = (array) json_decode($config); @@ -42,355 +46,17 @@ $languages = $config_langs; } -// Process the index file, just once. -$keys = file_get_contents('langindex.json'); -$keys = (array) json_decode($keys); - -foreach ($keys as $key => $value) { - $map = new StdClass(); - if ($value == 'local_moodlemobileapp') { - $map->file = $value; - $map->string = $key; - } else { - $exp = explode('/', $value, 2); - $map->file = $exp[0]; - if (count($exp) == 2) { - $map->string = $exp[1]; - } else { - $exp = explode('.', $key, 3); - - if (count($exp) == 3) { - $map->string = $exp[2]; - } else { - $map->string = $exp[1]; - } - } - } - - $keys[$key] = $map; -} -$total = count ($keys); - -echo "Total strings to translate $total\n"; +$keys = get_langindex_keys(); -$add_langs = array(); -// Process the languages. -foreach ($languages as $lang) { - $ok = build_lang($lang, $keys, $total); - if ($ok) { - $add_langs[$lang] = $lang; - } -} +$added_langs = build_languages($languages, $keys); if ($forcedetect) { - echo "\n\n\n"; - - $all_languages = glob(LANGPACKSFOLDER.'/*' , GLOB_ONLYDIR); - function get_lang_from_dir($dir) { - return str_replace('_', '-', explode('/', $dir)[3]); - } - $all_languages = array_map('get_lang_from_dir', $all_languages); - $detect_lang = array_diff($all_languages, $languages); - $new_langs = array(); - foreach ($detect_lang as $lang) { - $new = detect_lang($lang, $keys, $total); - if ($new) { - $new_langs[$lang] = $lang; - } - } + $new_langs = detect_languages($languages, $keys); if (!empty($new_langs)) { echo "\n\n\nThe following languages are going to be added\n\n\n"; - foreach ($new_langs as $lang) { - $ok = build_lang($lang, $keys, $total); - if ($ok) { - $add_langs[$lang] = $lang; - } - } - add_langs_to_config($add_langs, $config); - } -} else { - add_langs_to_config($add_langs, $config); -} - -function add_langs_to_config($langs, $config) { - $changed = false; - $config_langs = get_object_vars($config['languages']); - foreach ($langs as $lang) { - if (!isset($config_langs[$lang])) { - $langfoldername = str_replace('-', '_', $lang); - - $string = []; - include(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php'); - $config['languages']->$lang = $string['thislanguage']; - $changed = true; - } - } - - if ($changed) { - // Sort languages by key. - $config['languages'] = json_decode( json_encode( $config['languages'] ), true ); - ksort($config['languages']); - $config['languages'] = json_decode( json_encode( $config['languages'] ), false ); - file_put_contents(CONFIG, json_encode($config, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); - } -} - -function build_lang($lang, $keys, $total) { - $local = 0; - $langFile = false; - $translations = []; - $langfoldername = str_replace('-', '_', $lang); - - if (!is_dir(LANGPACKSFOLDER.'/'.$langfoldername) || !is_file(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php')) { - echo "Cannot translate $langfoldername, folder not found"; - return false; - } - - $string = []; - include(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php'); - $parent = isset($string['parentlanguage']) ? $string['parentlanguage'] : ""; - - echo "Processing $lang"; - if ($parent != "" && $parent != $lang) { - echo "($parent)"; - } - - - // Add the translation to the array. - foreach ($keys as $key => $value) { - $file = LANGPACKSFOLDER.'/'.$langfoldername.'/'.$value->file.'.php'; - // Apply translations. - if (!file_exists($file)) { - if (TOTRANSLATE) { - echo "\n\t\To translate $value->string on $value->file"; - } - continue; - } - - $string = []; - include($file); - - if (!isset($string[$value->string]) || ($lang == 'en' && $value->file == 'local_moodlemobileapp')) { - // Not yet translated. Do not override. - if (!$langFile) { - // Load lang files just once. - $langFile = file_get_contents(ASSETSPATH.$lang.'.json'); - $langFile = (array) json_decode($langFile); - } - if (is_array($langFile) && isset($langFile[$key])) { - $translations[$key] = $langFile[$key]; - $local++; - } - if (TOTRANSLATE) { - echo "\n\t\tTo translate $value->string on $value->file"; - } - continue; - } else { - $text = $string[$value->string]; - } - - if ($value->file != 'local_moodlemobileapp') { - $text = str_replace('$a->', '$a.', $text); - $text = str_replace('{$a', '{{$a', $text); - $text = str_replace('}', '}}', $text); - // Prevent double. - $text = str_replace(array('{{{', '}}}'), array('{{', '}}'), $text); - } else { - $local++; - } - - $translations[$key] = html_entity_decode($text); - } - - // Sort and save. - ksort($translations); - file_put_contents(ASSETSPATH.$lang.'.json', str_replace('\/', '/', json_encode($translations, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT))); - - $success = count($translations); - $percentage = floor($success/$total *100); - echo "\t\t$success of $total -> $percentage% ($local local)\n"; - - if ($lang == 'en') { - generate_local_moodlemobileapp($keys, $translations); - override_component_lang_files($keys, $translations); - } - - return true; -} - -function detect_lang($lang, $keys, $total) { - $success = 0; - $local = 0; - $langfoldername = str_replace('-', '_', $lang); - - if (!is_dir(LANGPACKSFOLDER.'/'.$langfoldername) || !is_file(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php')) { - echo "Cannot translate $langfoldername, folder not found"; - return false; - } - - $string = []; - include(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php'); - $parent = isset($string['parentlanguage']) ? $string['parentlanguage'] : ""; - if (!isset($string['thislanguage'])) { - echo "Cannot translate $langfoldername, name not found"; - return false; - } - - echo "Checking $lang"; - if ($parent != "" && $parent != $lang) { - echo "($parent)"; - } - $langname = $string['thislanguage']; - echo " ".$langname." -D"; - - // Add the translation to the array. - foreach ($keys as $key => $value) { - $file = LANGPACKSFOLDER.'/'.$langfoldername.'/'.$value->file.'.php'; - // Apply translations. - if (!file_exists($file)) { - continue; - } - - $string = []; - include($file); - - if (!isset($string[$value->string])) { - continue; - } else { - $text = $string[$value->string]; - } - - if ($value->file == 'local_moodlemobileapp') { - $local++; - } - - $success++; - } - - $percentage = floor($success/$total *100); - echo "\t\t$success of $total -> $percentage% ($local local)"; - if (($percentage > 75 && $local > 50) || ($percentage > 50 && $local > 75)) { - echo " \t DETECTED\n"; - return true; - } - echo "\n"; - - return false; -} - -function save_key($key, $value, $path) { - $filePath = $path . '/en.json'; - - $file = file_get_contents($filePath); - $file = (array) json_decode($file); - $value = html_entity_decode($value); - if ($file[$key] != $value) { - $file[$key] = $value; - ksort($file); - file_put_contents($filePath, str_replace('\/', '/', json_encode($file, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT))); + $added_langs = build_languages($new_langs, $keys, $added_langs); } } -function override_component_lang_files($keys, $translations) { - echo "Override component lang files.\n"; - foreach ($translations as $key => $value) { - $path = '../src/'; - $exp = explode('.', $key, 3); - - $type = $exp[0]; - if (count($exp) == 3) { - $component = $exp[1]; - $plainid = $exp[2]; - } else { - $component = 'moodle'; - $plainid = $exp[1]; - } - switch($type) { - case 'core': - case 'addon': - switch($component) { - case 'moodle': - $path .= 'lang'; - break; - default: - $path .= $type.'/'.str_replace('_', '/', $component).'/lang'; - break; - } - break; - case 'assets': - $path .= $type.'/'.$component; - break; - - } - - if (is_file($path.'/en.json')) { - save_key($plainid, $value, $path); - } - } -} - -/** - * Generates local moodle mobile app file to update languages in AMOS. - * - * @param [array] $keys Translation keys. - * @param [array] $translations English translations. - */ -function generate_local_moodlemobileapp($keys, $translations) { - echo "Generate local_moodlemobileapp.\n"; - $string = '. - -/** - * Version details. - * - * @package local - * @subpackage moodlemobileapp - * @copyright 2014 Juan Leyva - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -$string[\'appstoredescription\'] = \'NOTE: This official Moodle Mobile app will ONLY work with Moodle sites that have been set up to allow it. Please talk to your Moodle administrator if you have any problems connecting. - -If your Moodle site has been configured correctly, you can use this app to: - -- browse the content of your courses, even when offline -- receive instant notifications of messages and other events -- quickly find and contact other people in your courses -- upload images, audio, videos and other files from your mobile device -- view your course grades -- and more! - -Please see http://docs.moodle.org/en/Mobile_app for all the latest information. - -We’d really appreciate any good reviews about the functionality so far, and your suggestions on what else you want this app to do! - -The app requires the following permissions: -Record audio - For recording audio to upload to Moodle -Read and modify the contents of your SD card - Contents are downloaded to the SD Card so you can see them offline -Network access - To be able to connect with your Moodle site and check if you are connected or not to switch to offline mode -Run at startup - So you receive local notifications even when the app is running in the background -Prevent phone from sleeping - So you can receive push notifications anytime\';'."\n"; - foreach ($keys as $key => $value) { - if (isset($translations[$key]) && $value->file == 'local_moodlemobileapp') { - $string .= '$string[\''.$key.'\'] = \''.str_replace("'", "\'", $translations[$key]).'\';'."\n"; - } - } - $string .= '$string[\'pluginname\'] = \'Moodle Mobile language strings\';'."\n"; - - file_put_contents('../../moodle-local_moodlemobileapp/lang/en/local_moodlemobileapp.php', $string."\n"); -} - +add_langs_to_config($added_langs, $config); From 58b2e3315622b5a895da6f803e98013195250b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 23 Jan 2019 16:56:06 +0100 Subject: [PATCH 016/241] MOBILE-1332 notes: Delete notes --- scripts/langindex.json | 2 + .../components/list/addon-notes-list.html | 6 +++ src/addon/notes/components/list/list.ts | 51 ++++++++++++++++++- src/addon/notes/lang/en.json | 2 + src/addon/notes/providers/notes.ts | 21 ++++++++ src/assets/lang/en.json | 2 + 6 files changed, 83 insertions(+), 1 deletion(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 7381c6fc8b7..57d11d064c2 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -898,7 +898,9 @@ "addon.mod_workshop_assessment_rubric.mustchooseone": "workshopform_rubric", "addon.notes.addnewnote": "notes", "addon.notes.coursenotes": "notes", + "addon.notes.deleteconfirm": "notes", "addon.notes.eventnotecreated": "notes", + "addon.notes.eventnotedeleted": "notes", "addon.notes.nonotes": "notes", "addon.notes.note": "notes", "addon.notes.notes": "notes", diff --git a/src/addon/notes/components/list/addon-notes-list.html b/src/addon/notes/components/list/addon-notes-list.html index 2c069ca78a8..fbbb27c7898 100644 --- a/src/addon/notes/components/list/addon-notes-list.html +++ b/src/addon/notes/components/list/addon-notes-list.html @@ -1,4 +1,7 @@ + @@ -37,6 +40,9 @@

{{user.fullname}}

{{note.userfullname}}

{{note.lastmodified | coreDateDayOrTime}}

{{ 'core.notsent' | translate }}

+ diff --git a/src/addon/notes/components/list/list.ts b/src/addon/notes/components/list/list.ts index 0150edbfd01..78f439efd01 100644 --- a/src/addon/notes/components/list/list.ts +++ b/src/addon/notes/components/list/list.ts @@ -14,11 +14,13 @@ import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Content, ModalController } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUserProvider } from '@core/user/providers/user'; +import { coreSlideInOut } from '@classes/animations'; import { AddonNotesProvider } from '../../providers/notes'; import { AddonNotesSyncProvider } from '../../providers/notes-sync'; @@ -28,6 +30,7 @@ import { AddonNotesSyncProvider } from '../../providers/notes-sync'; @Component({ selector: 'addon-notes-list', templateUrl: 'addon-notes-list.html', + animations: [coreSlideInOut] }) export class AddonNotesListComponent implements OnInit, OnDestroy { @Input() courseId: number; @@ -44,11 +47,14 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { hasOffline = false; notesLoaded = false; user: any; + showDelete = false; + canDeleteNotes = false; + currentUserId: number; constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, private modalCtrl: ModalController, private notesProvider: AddonNotesProvider, private notesSync: AddonNotesSyncProvider, - private userProvider: CoreUserProvider) { + private userProvider: CoreUserProvider, private translate: TranslateService) { // Refresh data if notes are synchronized automatically. this.syncObserver = eventsProvider.on(AddonNotesSyncProvider.AUTO_SYNCED, (data) => { if (data.courseId == this.courseId) { @@ -64,6 +70,8 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { this.fetchNotes(false); } }, sitesProvider.getCurrentSiteId()); + + this.currentUserId = sitesProvider.getCurrentSiteUserId(); } /** @@ -111,6 +119,14 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { }).catch((message) => { this.domUtils.showErrorModal(message); }).finally(() => { + let canDelete = this.notes && this.notes.length > 0; + if (canDelete && this.type == 'personal') { + canDelete = this.notes.find((note) => { + return note.usermodified == this.currentUserId; + }); + } + this.canDeleteNotes = canDelete; + this.notesLoaded = true; this.refreshIcon = 'refresh'; this.syncIcon = 'sync'; @@ -151,6 +167,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { /** * Add a new Note to user and course. + * * @param {Event} e Event. */ addNote(e: Event): void { @@ -173,6 +190,38 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { modal.present(); } + /** + * Delete a note. + * + * @param {Event} e Click event. + * @param {any} note Note to delete. + */ + deleteNote(e: Event, note: any): void { + e.preventDefault(); + e.stopPropagation(); + + this.domUtils.showConfirm(this.translate.instant('addon.notes.deleteconfirm')).then(() => { + this.notesProvider.deleteNote(note).then(() => { + this.showDelete = false; + + this.refreshNotes(true); + + this.domUtils.showToast('addon.notes.eventnotedeleted', true, 3000); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Delete note failed.'); + }); + }).catch(() => { + // User cancelled, nothing to do. + }); + } + + /** + * Toggle delete. + */ + toggleDelete(): void { + this.showDelete = !this.showDelete; + } + /** * Tries to synchronize course notes. * diff --git a/src/addon/notes/lang/en.json b/src/addon/notes/lang/en.json index 3317484cdd8..c8256d0c4b3 100644 --- a/src/addon/notes/lang/en.json +++ b/src/addon/notes/lang/en.json @@ -1,7 +1,9 @@ { "addnewnote": "Add a new note", "coursenotes": "Course notes", + "deleteconfirm": "Delete this note?", "eventnotecreated": "Note created", + "eventnotedeleted": "Note deleted", "nonotes": "There are no notes of this type yet", "note": "Note", "notes": "Notes", diff --git a/src/addon/notes/providers/notes.ts b/src/addon/notes/providers/notes.ts index 006a78093dd..52caa2c37b7 100644 --- a/src/addon/notes/providers/notes.ts +++ b/src/addon/notes/providers/notes.ts @@ -133,6 +133,27 @@ export class AddonNotesProvider { }); } + /** + * Delete a note. + * + * @param {any} note Note object to delete. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + deleteNote(note: any, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + if (typeof note.offline != 'undefined' && note.offline) { + return this.notesOffline.deleteNote(note.userid, note.content, note.created, site.id); + } + + const data = { + notes: [note.id] + }; + + return site.write('core_notes_delete_notes', data); + }); + } + /** * Returns whether or not the notes plugin is enabled for a certain site. * diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 7ed0d48128f..ef8b0d722df 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -898,7 +898,9 @@ "addon.mod_workshop_assessment_rubric.mustchooseone": "You have to select one of these items", "addon.notes.addnewnote": "Add a new note", "addon.notes.coursenotes": "Course notes", + "addon.notes.deleteconfirm": "Delete this note?", "addon.notes.eventnotecreated": "Note created", + "addon.notes.eventnotedeleted": "Note deleted", "addon.notes.nonotes": "There are no notes of this type yet", "addon.notes.note": "Note", "addon.notes.notes": "Notes", From 26a1fad8c8e1009f1a20f39f8005890e3e3d504d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 8 May 2019 11:33:58 +0200 Subject: [PATCH 017/241] MOBILE-3014 block: Support for only title block type --- .../providers/block-handler.ts | 2 +- .../myoverview/providers/block-handler.ts | 2 +- .../providers/block-handler.ts | 2 +- .../providers/block-handler.ts | 2 +- .../sitemainmenu/providers/block-handler.ts | 2 +- .../starredcourses/providers/block-handler.ts | 2 +- .../block/timeline/providers/block-handler.ts | 2 +- .../block/classes/base-block-component.ts | 9 +++- src/core/block/classes/base-block-handler.ts | 2 +- src/core/block/components/block/block.ts | 5 +- .../block/components/components.module.ts | 12 ++++- .../core-block-only-title.html | 3 ++ .../only-title-block/only-title-block.ts | 50 +++++++++++++++++++ src/core/block/providers/delegate.ts | 15 +++++- 14 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 src/core/block/components/only-title-block/core-block-only-title.html create mode 100644 src/core/block/components/only-title-block/only-title-block.ts diff --git a/src/addon/block/activitymodules/providers/block-handler.ts b/src/addon/block/activitymodules/providers/block-handler.ts index 0024324c991..752d23a47aa 100644 --- a/src/addon/block/activitymodules/providers/block-handler.ts +++ b/src/addon/block/activitymodules/providers/block-handler.ts @@ -38,7 +38,7 @@ export class AddonBlockActivityModulesHandler extends CoreBlockBaseHandler { * @param {number} instanceId The instance ID associated with the context level. * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number) + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) : CoreBlockHandlerData | Promise { return { diff --git a/src/addon/block/myoverview/providers/block-handler.ts b/src/addon/block/myoverview/providers/block-handler.ts index 5f723467cfd..e0a593b9a18 100644 --- a/src/addon/block/myoverview/providers/block-handler.ts +++ b/src/addon/block/myoverview/providers/block-handler.ts @@ -50,7 +50,7 @@ export class AddonBlockMyOverviewHandler extends CoreBlockBaseHandler { * @param {number} instanceId The instance ID associated with the context level. * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number) + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) : CoreBlockHandlerData | Promise { return { diff --git a/src/addon/block/recentlyaccessedcourses/providers/block-handler.ts b/src/addon/block/recentlyaccessedcourses/providers/block-handler.ts index a1117bd5d6b..3af9cf0dd36 100644 --- a/src/addon/block/recentlyaccessedcourses/providers/block-handler.ts +++ b/src/addon/block/recentlyaccessedcourses/providers/block-handler.ts @@ -38,7 +38,7 @@ export class AddonBlockRecentlyAccessedCoursesHandler extends CoreBlockBaseHandl * @param {number} instanceId The instance ID associated with the context level. * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number) + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) : CoreBlockHandlerData | Promise { return { diff --git a/src/addon/block/recentlyaccesseditems/providers/block-handler.ts b/src/addon/block/recentlyaccesseditems/providers/block-handler.ts index 2963017e876..dfadcc54271 100644 --- a/src/addon/block/recentlyaccesseditems/providers/block-handler.ts +++ b/src/addon/block/recentlyaccesseditems/providers/block-handler.ts @@ -38,7 +38,7 @@ export class AddonBlockRecentlyAccessedItemsHandler extends CoreBlockBaseHandler * @param {number} instanceId The instance ID associated with the context level. * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number) + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) : CoreBlockHandlerData | Promise { return { diff --git a/src/addon/block/sitemainmenu/providers/block-handler.ts b/src/addon/block/sitemainmenu/providers/block-handler.ts index 8c699aff0ea..dcaf4fe01a4 100644 --- a/src/addon/block/sitemainmenu/providers/block-handler.ts +++ b/src/addon/block/sitemainmenu/providers/block-handler.ts @@ -38,7 +38,7 @@ export class AddonBlockSiteMainMenuHandler extends CoreBlockBaseHandler { * @param {number} instanceId The instance ID associated with the context level. * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number) + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) : CoreBlockHandlerData | Promise { return { diff --git a/src/addon/block/starredcourses/providers/block-handler.ts b/src/addon/block/starredcourses/providers/block-handler.ts index 7675876b127..c7abc34854a 100644 --- a/src/addon/block/starredcourses/providers/block-handler.ts +++ b/src/addon/block/starredcourses/providers/block-handler.ts @@ -38,7 +38,7 @@ export class AddonBlockStarredCoursesHandler extends CoreBlockBaseHandler { * @param {number} instanceId The instance ID associated with the context level. * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number) + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) : CoreBlockHandlerData | Promise { return { diff --git a/src/addon/block/timeline/providers/block-handler.ts b/src/addon/block/timeline/providers/block-handler.ts index 2e89d60fbe5..8973fbc5dae 100644 --- a/src/addon/block/timeline/providers/block-handler.ts +++ b/src/addon/block/timeline/providers/block-handler.ts @@ -55,7 +55,7 @@ export class AddonBlockTimelineHandler extends CoreBlockBaseHandler { * @param {number} instanceId The instance ID associated with the context level. * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number) + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) : CoreBlockHandlerData | Promise { return { diff --git a/src/core/block/classes/base-block-component.ts b/src/core/block/classes/base-block-component.ts index 56cbad4f6c3..46dc2c33bc5 100644 --- a/src/core/block/classes/base-block-component.ts +++ b/src/core/block/classes/base-block-component.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injector, OnInit } from '@angular/core'; +import { Injector, OnInit, Input } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; @@ -20,6 +20,13 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom'; * Template class to easily create components for blocks. */ export class CoreBlockBaseComponent implements OnInit { + @Input() title: string; // The block title. + @Input() block: any; // 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() link: string; // Link to go when clicked. + @Input() linkParams: string; // Link params to go when clicked. + loaded: boolean; // If the component has been loaded. protected fetchContentDefaultError: string; // Default error to show when loading contents. diff --git a/src/core/block/classes/base-block-handler.ts b/src/core/block/classes/base-block-handler.ts index 85fe4999558..695d8182e79 100644 --- a/src/core/block/classes/base-block-handler.ts +++ b/src/core/block/classes/base-block-handler.ts @@ -47,7 +47,7 @@ export class CoreBlockBaseHandler implements CoreBlockHandler { * @param {number} instanceId The instance ID associated with the context level. * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number) + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) : CoreBlockHandlerData | Promise { // To be overridden. diff --git a/src/core/block/components/block/block.ts b/src/core/block/components/block/block.ts index fffba568310..faf8aa3be89 100644 --- a/src/core/block/components/block/block.ts +++ b/src/core/block/components/block/block.ts @@ -33,7 +33,6 @@ export class CoreBlockComponent implements OnInit, OnDestroy { @Input() instanceId: number; // The instance ID associated with the context level. @Input() extraData: any; // Any extra data to be passed to the block. - title: string; // The title of the block. componentClass: any; // The class of the component to render. data: any = {}; // Data to pass to the component. class: string; // CSS class to apply to the block. @@ -80,15 +79,17 @@ export class CoreBlockComponent implements OnInit, OnDestroy { return; } - this.title = data.title; this.class = data.class; this.componentClass = data.component; // Set up the data needed by the block component. this.data = Object.assign({ + title: data.title, block: this.block, contextLevel: this.contextLevel, instanceId: this.instanceId, + link: data.link || null, + linkParams: data.linkParams || null, }, this.extraData || {}, data.componentData || {}); }).catch(() => { // Ignore errors. diff --git a/src/core/block/components/components.module.ts b/src/core/block/components/components.module.ts index 512729b8e88..896804473f4 100644 --- a/src/core/block/components/components.module.ts +++ b/src/core/block/components/components.module.ts @@ -16,23 +16,31 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; +import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreBlockComponent } from './block/block'; +import { CoreBlockOnlyTitleComponent } from './only-title-block/only-title-block'; import { CoreComponentsModule } from '@components/components.module'; @NgModule({ declarations: [ - CoreBlockComponent + CoreBlockComponent, + CoreBlockOnlyTitleComponent ], imports: [ CommonModule, IonicModule, + CoreDirectivesModule, TranslateModule.forChild(), CoreComponentsModule ], providers: [ ], exports: [ - CoreBlockComponent + CoreBlockComponent, + CoreBlockOnlyTitleComponent + ], + entryComponents: [ + CoreBlockOnlyTitleComponent ] }) export class CoreBlockComponentsModule {} diff --git a/src/core/block/components/only-title-block/core-block-only-title.html b/src/core/block/components/only-title-block/core-block-only-title.html new file mode 100644 index 00000000000..287592371e4 --- /dev/null +++ b/src/core/block/components/only-title-block/core-block-only-title.html @@ -0,0 +1,3 @@ + +

{{ title | translate }}

+
\ No newline at end of file diff --git a/src/core/block/components/only-title-block/only-title-block.ts b/src/core/block/components/only-title-block/only-title-block.ts new file mode 100644 index 00000000000..a128c44d130 --- /dev/null +++ b/src/core/block/components/only-title-block/only-title-block.ts @@ -0,0 +1,50 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Injector, OnInit, Component } from '@angular/core'; +import { CoreBlockBaseComponent } from '../../classes/base-block-component'; +import { CoreLoginHelperProvider } from '@core/login/providers/helper'; + +/** + * Component to render blocks with only a title and link. + */ +@Component({ + selector: 'core-block-only-title', + templateUrl: 'core-block-only-title.html' +}) +export class CoreBlockOnlyTitleComponent extends CoreBlockBaseComponent implements OnInit { + + protected loginHelper: CoreLoginHelperProvider; + + constructor(injector: Injector) { + super(injector, 'CoreBlockOnlyTitleComponent'); + this.loginHelper = injector.get(CoreLoginHelperProvider); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + super.ngOnInit(); + + this.fetchContentDefaultError = 'Error getting ' + this.block.contents.title + ' data.'; + } + + /** + * Go to the block page. + */ + gotoBlock(): void { + this.loginHelper.redirect(this.link, this.linkParams); + } +} diff --git a/src/core/block/providers/delegate.ts b/src/core/block/providers/delegate.ts index 47b90ecda76..b4e6fcf4038 100644 --- a/src/core/block/providers/delegate.ts +++ b/src/core/block/providers/delegate.ts @@ -73,6 +73,18 @@ export interface CoreBlockHandlerData { * @type {any} */ componentData?: any; + + /** + * Link to go when showing only title. + * @type {string} + */ + link?: string; + + /** + * Params of the link. + * @type {[type]} + */ + linkParams?: any; } /** @@ -127,7 +139,8 @@ export class CoreBlockDelegate extends CoreDelegate { * @return {Promise} Promise resolved with the display data. */ getBlockDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number): Promise { - return Promise.resolve(this.executeFunctionOnEnabled(block.name, 'getDisplayData', [injector, block])); + return Promise.resolve(this.executeFunctionOnEnabled(block.name, 'getDisplayData', + [injector, block, contextLevel, instanceId])); } /** From 9a5f56738746b142678e7355451fa822758d5619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 6 May 2019 10:51:45 +0200 Subject: [PATCH 018/241] MOBILE-3014 course: Add course blocks tab --- scripts/langindex.json | 1 + src/assets/lang/en.json | 1 + src/core/block/block.module.ts | 14 ++- .../block/components/components.module.ts | 10 +- .../core-block-course-blocks.html | 15 +++ .../components/course-blocks/course-blocks.ts | 100 ++++++++++++++++++ src/core/block/lang/en.json | 3 + .../block/providers/course-option-handler.ts | 88 +++++++++++++++ 8 files changed, 227 insertions(+), 5 deletions(-) create mode 100644 src/core/block/components/course-blocks/core-block-course-blocks.html create mode 100644 src/core/block/components/course-blocks/course-blocks.ts create mode 100644 src/core/block/lang/en.json create mode 100644 src/core/block/providers/course-option-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 7381c6fc8b7..5abbddcb413 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1231,6 +1231,7 @@ "core.answered": "quiz", "core.areyousure": "moodle", "core.back": "moodle", + "core.block.blocks": "moodle", "core.cancel": "moodle", "core.cannotconnect": "local_moodlemobileapp", "core.cannotdownloadfiles": "local_moodlemobileapp", diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 7ed0d48128f..7608fe27719 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1231,6 +1231,7 @@ "core.answered": "Answered", "core.areyousure": "Are you sure?", "core.back": "Back", + "core.block.blocks": "Blocks", "core.cancel": "Cancel", "core.cannotconnect": "Cannot connect: Verify that you have correctly typed the URL and that your site uses Moodle 2.4 or later.", "core.cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.", diff --git a/src/core/block/block.module.ts b/src/core/block/block.module.ts index 2448c6e1dd1..764d27fb961 100644 --- a/src/core/block/block.module.ts +++ b/src/core/block/block.module.ts @@ -15,6 +15,9 @@ import { NgModule } from '@angular/core'; import { CoreBlockDelegate } from './providers/delegate'; import { CoreBlockDefaultHandler } from './providers/default-block-handler'; +import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; +import { CoreBlockCourseBlocksCourseOptionHandler } from './providers/course-option-handler'; +import { CoreBlockComponentsModule } from './components/components.module'; // List of providers (without handlers). export const CORE_BLOCK_PROVIDERS: any[] = [ @@ -24,11 +27,18 @@ export const CORE_BLOCK_PROVIDERS: any[] = [ @NgModule({ declarations: [], imports: [ + CoreBlockComponentsModule ], providers: [ CoreBlockDelegate, - CoreBlockDefaultHandler + CoreBlockDefaultHandler, + CoreBlockCourseBlocksCourseOptionHandler ], exports: [] }) -export class CoreBlockModule {} +export class CoreBlockModule { + constructor(courseOptionHandler: CoreBlockCourseBlocksCourseOptionHandler, + courseOptionsDelegate: CoreCourseOptionsDelegate) { + courseOptionsDelegate.registerHandler(courseOptionHandler); + } +} diff --git a/src/core/block/components/components.module.ts b/src/core/block/components/components.module.ts index 896804473f4..33c80abcff0 100644 --- a/src/core/block/components/components.module.ts +++ b/src/core/block/components/components.module.ts @@ -19,12 +19,14 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreBlockComponent } from './block/block'; import { CoreBlockOnlyTitleComponent } from './only-title-block/only-title-block'; +import { CoreBlockCourseBlocksComponent } from './course-blocks/course-blocks'; import { CoreComponentsModule } from '@components/components.module'; @NgModule({ declarations: [ CoreBlockComponent, - CoreBlockOnlyTitleComponent + CoreBlockOnlyTitleComponent, + CoreBlockCourseBlocksComponent ], imports: [ CommonModule, @@ -37,10 +39,12 @@ import { CoreComponentsModule } from '@components/components.module'; ], exports: [ CoreBlockComponent, - CoreBlockOnlyTitleComponent + CoreBlockOnlyTitleComponent, + CoreBlockCourseBlocksComponent ], entryComponents: [ - CoreBlockOnlyTitleComponent + CoreBlockOnlyTitleComponent, + CoreBlockCourseBlocksComponent ] }) export class CoreBlockComponentsModule {} diff --git a/src/core/block/components/course-blocks/core-block-course-blocks.html b/src/core/block/components/course-blocks/core-block-course-blocks.html new file mode 100644 index 00000000000..cf9457d5810 --- /dev/null +++ b/src/core/block/components/course-blocks/core-block-course-blocks.html @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/core/block/components/course-blocks/course-blocks.ts b/src/core/block/components/course-blocks/course-blocks.ts new file mode 100644 index 00000000000..8adbb5e4eb4 --- /dev/null +++ b/src/core/block/components/course-blocks/course-blocks.ts @@ -0,0 +1,100 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, ViewChildren, Input, OnInit, QueryList } from '@angular/core'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreBlockComponent } from '../block/block'; +import { CoreBlockDelegate } from '../../providers/delegate'; +import { CoreCourseProvider } from '@core/course/providers/course'; + +/** + * Component that displays the list of course blocks. + */ +@Component({ + selector: 'core-block-course-blocks', + templateUrl: 'core-block-course-blocks.html', +}) +export class CoreBlockCourseBlocksComponent implements OnInit { + + @Input() courseId: number; + + @ViewChildren(CoreBlockComponent) blocksComponents: QueryList; + + dataLoaded = false; + hasContent: boolean; + hasSupportedBlock: boolean; + blocks = []; + + constructor(private domUtils: CoreDomUtilsProvider, private courseProvider: CoreCourseProvider, + private blockDelegate: CoreBlockDelegate) { + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.loadContent().finally(() => { + this.dataLoaded = true; + }); + } + + /** + * Refresh the data. + * + * @param {any} refresher Refresher. + */ + doRefresh(refresher: any): void { + const promises = []; + + if (this.courseProvider.canGetCourseBlocks()) { + promises.push(this.courseProvider.invalidateCourseBlocks(this.courseId)); + } + + // Invalidate the blocks. + this.blocksComponents.forEach((blockComponent) => { + promises.push(blockComponent.invalidate().catch(() => { + // Ignore errors. + })); + }); + + Promise.all(promises).finally(() => { + this.loadContent().finally(() => { + refresher.complete(); + }); + }); + } + + /** + * Convenience function to fetch the data. + * + * @return {Promise} Promise resolved when done. + */ + protected loadContent(): Promise { + // Get site home blocks. + const canGetBlocks = this.courseProvider.canGetCourseBlocks(), + promise = canGetBlocks ? this.courseProvider.getCourseBlocks(this.courseId) : Promise.reject(null); + + return promise.then((blocks) => { + this.blocks = blocks; + this.hasSupportedBlock = this.blockDelegate.hasSupportedBlock(blocks); + + }).catch((error) => { + if (canGetBlocks) { + this.domUtils.showErrorModal(error); + } + this.blocks = []; + }); + + } +} diff --git a/src/core/block/lang/en.json b/src/core/block/lang/en.json new file mode 100644 index 00000000000..9b136b8ee2a --- /dev/null +++ b/src/core/block/lang/en.json @@ -0,0 +1,3 @@ +{ + "blocks": "Blocks" +} \ No newline at end of file diff --git a/src/core/block/providers/course-option-handler.ts b/src/core/block/providers/course-option-handler.ts new file mode 100644 index 00000000000..c7e298c9cb8 --- /dev/null +++ b/src/core/block/providers/course-option-handler.ts @@ -0,0 +1,88 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '@core/course/providers/options-delegate'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreBlockCourseBlocksComponent } from '../components/course-blocks/course-blocks'; + +/** + * Course nav handler. + */ +@Injectable() +export class CoreBlockCourseBlocksCourseOptionHandler implements CoreCourseOptionsHandler { + name = 'CoreCourseBlocks'; + priority = 700; + + constructor(private courseProvider: CoreCourseProvider) {} + + /** + * Should invalidate the data to determine if the handler is enabled for a certain course. + * + * @param {number} courseId The course ID. + * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. + * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. + * @return {Promise} Promise resolved when done. + */ + invalidateEnabledForCourse(courseId: number, navOptions?: any, admOptions?: any): Promise { + return this.courseProvider.invalidateCourseBlocks(courseId); + } + + /** + * Check if the handler is enabled on a site level. + * + * @return {boolean} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return this.courseProvider.canGetCourseBlocks(); + } + + /** + * Whether or not the handler is enabled for a certain course. + * + * @param {number} courseId The course ID. + * @param {any} accessData Access type and data. Default, guest, ... + * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. + * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise { + return true; + } + + /** + * Returns the data needed to render the handler. + * + * @param {Injector} injector Injector. + * @param {number} courseId The course ID. + * @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise { + return { + title: 'core.block.blocks', + class: 'core-course-blocks-handler', + component: CoreBlockCourseBlocksComponent + }; + } + + /** + * Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline. + * + * @param {any} course The course. + * @return {Promise} Promise resolved when done. + */ + prefetch(course: any): Promise { + return this.courseProvider.getCourseBlocks(course.id); + } +} From 2ad5daec1c0392204d1495ef55ef66118c272d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 7 May 2019 13:31:48 +0200 Subject: [PATCH 019/241] MOBILE-3014 block: Add Calendar block feature --- scripts/langindex.json | 1 + .../calendarmonth/calendarmonth.module.ts | 38 ++++++++++++++ src/addon/block/calendarmonth/lang/en.json | 3 ++ .../calendarmonth/providers/block-handler.ts | 52 +++++++++++++++++++ src/addon/calendar/pages/list/list.ts | 8 +++ src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + 7 files changed, 105 insertions(+) create mode 100644 src/addon/block/calendarmonth/calendarmonth.module.ts create mode 100644 src/addon/block/calendarmonth/lang/en.json create mode 100644 src/addon/block/calendarmonth/providers/block-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 5abbddcb413..548a3173f68 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -27,6 +27,7 @@ "addon.badges.version": "badges", "addon.badges.warnexpired": "badges", "addon.block_activitymodules.pluginname": "block_activity_modules", + "addon.block_calendarmonth.pluginname": "block_calendar_month", "addon.block_myoverview.all": "block_myoverview", "addon.block_myoverview.favourites": "block_myoverview", "addon.block_myoverview.future": "block_myoverview", diff --git a/src/addon/block/calendarmonth/calendarmonth.module.ts b/src/addon/block/calendarmonth/calendarmonth.module.ts new file mode 100644 index 00000000000..e3204ae8f66 --- /dev/null +++ b/src/addon/block/calendarmonth/calendarmonth.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockCalendarMonthHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockCalendarMonthHandler + ] +}) +export class AddonBlockCalendarMonthModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockCalendarMonthHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/calendarmonth/lang/en.json b/src/addon/block/calendarmonth/lang/en.json new file mode 100644 index 00000000000..86a476c29e7 --- /dev/null +++ b/src/addon/block/calendarmonth/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Calendar" +} \ No newline at end of file diff --git a/src/addon/block/calendarmonth/providers/block-handler.ts b/src/addon/block/calendarmonth/providers/block-handler.ts new file mode 100644 index 00000000000..9dc4ce2ab5e --- /dev/null +++ b/src/addon/block/calendarmonth/providers/block-handler.ts @@ -0,0 +1,52 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockCalendarMonthHandler extends CoreBlockBaseHandler { + name = 'AddonBlockCalendarMonth'; + blockName = 'calendar_month'; + + constructor() { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + + return { + title: 'addon.block_calendarmonth.pluginname', + class: 'addon-block-calendar-month', + component: CoreBlockOnlyTitleComponent, + link: 'AddonCalendarListPage', + linkParams: contextLevel == 'course' ? { courseId: instanceId } : null + }; + } +} diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index cd8523d9007..7722dc1c4e1 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -53,6 +53,7 @@ export class AddonCalendarListPage implements OnDestroy { protected siteHomeId: number; protected obsDefaultTimeChange: any; protected eventId: number; + protected preSelectedCourseId: number; courses: any[]; eventsLoaded = false; @@ -81,6 +82,7 @@ export class AddonCalendarListPage implements OnDestroy { } this.eventId = navParams.get('eventId') || false; + this.preSelectedCourseId = navParams.get('courseId') || null; } /** @@ -118,6 +120,12 @@ export class AddonCalendarListPage implements OnDestroy { courses.unshift(this.allCourses); this.courses = courses; + if (this.preSelectedCourseId) { + this.filter.course = courses.find((course) => { + return course.id == this.preSelectedCourseId; + }); + } + return this.fetchEvents(refresh); }); } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 4a174e54242..e3b73426787 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -91,6 +91,7 @@ import { AddonCourseCompletionModule } from '@addon/coursecompletion/coursecompl import { AddonUserProfileFieldModule } from '@addon/userprofilefield/userprofilefield.module'; import { AddonFilesModule } from '@addon/files/files.module'; import { AddonBlockActivityModulesModule } from '@addon/block/activitymodules/activitymodules.module'; +import { AddonBlockCalendarMonthModule } from '@addon/block/calendarmonth/calendarmonth.module'; import { AddonBlockMyOverviewModule } from '@addon/block/myoverview/myoverview.module'; import { AddonBlockSiteMainMenuModule } from '@addon/block/sitemainmenu/sitemainmenu.module'; import { AddonBlockTimelineModule } from '@addon/block/timeline/timeline.module'; @@ -213,6 +214,7 @@ export const CORE_PROVIDERS: any[] = [ AddonUserProfileFieldModule, AddonFilesModule, AddonBlockActivityModulesModule, + AddonBlockCalendarMonthModule, AddonBlockMyOverviewModule, AddonBlockSiteMainMenuModule, AddonBlockTimelineModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 7608fe27719..cfe806f1dbe 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -27,6 +27,7 @@ "addon.badges.version": "Version", "addon.badges.warnexpired": "(This badge has expired!)", "addon.block_activitymodules.pluginname": "Activities", + "addon.block_calendarmonth.pluginname": "Calendar", "addon.block_myoverview.all": "All", "addon.block_myoverview.favourites": "Starred", "addon.block_myoverview.future": "Future", From ea7050104fea900c11afeddfbae3c7ae5b1ffd1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 7 May 2019 14:15:45 +0200 Subject: [PATCH 020/241] MOBILE-3014 block: Add Upcoming events block feature --- scripts/langindex.json | 1 + .../calendarupcoming.module.ts | 38 ++++++++++++++ src/addon/block/calendarupcoming/lang/en.json | 3 ++ .../providers/block-handler.ts | 52 +++++++++++++++++++ src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + 6 files changed, 97 insertions(+) create mode 100644 src/addon/block/calendarupcoming/calendarupcoming.module.ts create mode 100644 src/addon/block/calendarupcoming/lang/en.json create mode 100644 src/addon/block/calendarupcoming/providers/block-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 548a3173f68..e95ca82797c 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -28,6 +28,7 @@ "addon.badges.warnexpired": "badges", "addon.block_activitymodules.pluginname": "block_activity_modules", "addon.block_calendarmonth.pluginname": "block_calendar_month", + "addon.block_calendarupcoming.pluginname": "block_calendar_upcoming", "addon.block_myoverview.all": "block_myoverview", "addon.block_myoverview.favourites": "block_myoverview", "addon.block_myoverview.future": "block_myoverview", diff --git a/src/addon/block/calendarupcoming/calendarupcoming.module.ts b/src/addon/block/calendarupcoming/calendarupcoming.module.ts new file mode 100644 index 00000000000..345d1767223 --- /dev/null +++ b/src/addon/block/calendarupcoming/calendarupcoming.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockCalendarUpcomingHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockCalendarUpcomingHandler + ] +}) +export class AddonBlockCalendarUpcomingModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockCalendarUpcomingHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/calendarupcoming/lang/en.json b/src/addon/block/calendarupcoming/lang/en.json new file mode 100644 index 00000000000..4faba6dd20e --- /dev/null +++ b/src/addon/block/calendarupcoming/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": " Upcoming events" +} \ No newline at end of file diff --git a/src/addon/block/calendarupcoming/providers/block-handler.ts b/src/addon/block/calendarupcoming/providers/block-handler.ts new file mode 100644 index 00000000000..b7f4e8acdd2 --- /dev/null +++ b/src/addon/block/calendarupcoming/providers/block-handler.ts @@ -0,0 +1,52 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockCalendarUpcomingHandler extends CoreBlockBaseHandler { + name = 'AddonBlockCalendarUpcoming'; + blockName = 'calendar_upcoming'; + + constructor() { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + + return { + title: 'addon.block_calendarupcoming.pluginname', + class: 'addon-block-calendar-upcoming', + component: CoreBlockOnlyTitleComponent, + link: 'AddonCalendarListPage', + linkParams: contextLevel == 'course' ? { courseId: instanceId } : null + }; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e3b73426787..42dfd82d966 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -92,6 +92,7 @@ import { AddonUserProfileFieldModule } from '@addon/userprofilefield/userprofile import { AddonFilesModule } from '@addon/files/files.module'; import { AddonBlockActivityModulesModule } from '@addon/block/activitymodules/activitymodules.module'; import { AddonBlockCalendarMonthModule } from '@addon/block/calendarmonth/calendarmonth.module'; +import { AddonBlockCalendarUpcomingModule } from '@addon/block/calendarupcoming/calendarupcoming.module'; import { AddonBlockMyOverviewModule } from '@addon/block/myoverview/myoverview.module'; import { AddonBlockSiteMainMenuModule } from '@addon/block/sitemainmenu/sitemainmenu.module'; import { AddonBlockTimelineModule } from '@addon/block/timeline/timeline.module'; @@ -215,6 +216,7 @@ export const CORE_PROVIDERS: any[] = [ AddonFilesModule, AddonBlockActivityModulesModule, AddonBlockCalendarMonthModule, + AddonBlockCalendarUpcomingModule, AddonBlockMyOverviewModule, AddonBlockSiteMainMenuModule, AddonBlockTimelineModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index cfe806f1dbe..d59c8689523 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -28,6 +28,7 @@ "addon.badges.warnexpired": "(This badge has expired!)", "addon.block_activitymodules.pluginname": "Activities", "addon.block_calendarmonth.pluginname": "Calendar", + "addon.block_calendarupcoming.pluginname": " Upcoming events", "addon.block_myoverview.all": "All", "addon.block_myoverview.favourites": "Starred", "addon.block_myoverview.future": "Future", From e434b2a298e26a5331395bcd528c735f5f1cb46b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 7 May 2019 17:26:08 +0200 Subject: [PATCH 021/241] MOBILE-3014 block: Add Private files block feature --- scripts/langindex.json | 1 + src/addon/block/privatefiles/lang/en.json | 3 ++ .../block/privatefiles/privatefiles.module.ts | 38 ++++++++++++++ .../privatefiles/providers/block-handler.ts | 52 +++++++++++++++++++ src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + 6 files changed, 97 insertions(+) create mode 100644 src/addon/block/privatefiles/lang/en.json create mode 100644 src/addon/block/privatefiles/privatefiles.module.ts create mode 100644 src/addon/block/privatefiles/providers/block-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index e95ca82797c..b286cb6b579 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -40,6 +40,7 @@ "addon.block_myoverview.past": "block_myoverview", "addon.block_myoverview.pluginname": "block_myoverview", "addon.block_myoverview.title": "block_myoverview", + "addon.block_privatefiles.pluginname": "block_private_files", "addon.block_recentlyaccessedcourses.nocourses": "block_recentlyaccessedcourses", "addon.block_recentlyaccessedcourses.pluginname": "block_recentlyaccessedcourses", "addon.block_recentlyaccesseditems.noitems": "block_recentlyaccesseditems", diff --git a/src/addon/block/privatefiles/lang/en.json b/src/addon/block/privatefiles/lang/en.json new file mode 100644 index 00000000000..bba9d4bc04c --- /dev/null +++ b/src/addon/block/privatefiles/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Private files" +} \ No newline at end of file diff --git a/src/addon/block/privatefiles/privatefiles.module.ts b/src/addon/block/privatefiles/privatefiles.module.ts new file mode 100644 index 00000000000..c9617c3724c --- /dev/null +++ b/src/addon/block/privatefiles/privatefiles.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockPrivateFilesHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockPrivateFilesHandler + ] +}) +export class AddonBlockPrivateFilesModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockPrivateFilesHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/privatefiles/providers/block-handler.ts b/src/addon/block/privatefiles/providers/block-handler.ts new file mode 100644 index 00000000000..f0284df9747 --- /dev/null +++ b/src/addon/block/privatefiles/providers/block-handler.ts @@ -0,0 +1,52 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockPrivateFilesHandler extends CoreBlockBaseHandler { + name = 'AddonBlockPrivateFiles'; + blockName = 'private_files'; + + constructor() { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + + return { + title: 'addon.block_privatefiles.pluginname', + class: 'addon-block-private-files', + component: CoreBlockOnlyTitleComponent, + link: 'AddonFilesListPage', + linkParams: {root: 'my'} + }; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 42dfd82d966..8fd2e06d131 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -94,6 +94,7 @@ import { AddonBlockActivityModulesModule } from '@addon/block/activitymodules/ac import { AddonBlockCalendarMonthModule } from '@addon/block/calendarmonth/calendarmonth.module'; import { AddonBlockCalendarUpcomingModule } from '@addon/block/calendarupcoming/calendarupcoming.module'; import { AddonBlockMyOverviewModule } from '@addon/block/myoverview/myoverview.module'; +import { AddonBlockPrivateFilesModule } from '@addon/block/privatefiles/privatefiles.module'; import { AddonBlockSiteMainMenuModule } from '@addon/block/sitemainmenu/sitemainmenu.module'; import { AddonBlockTimelineModule } from '@addon/block/timeline/timeline.module'; import { AddonBlockRecentlyAccessedCoursesModule } from '@addon/block/recentlyaccessedcourses/recentlyaccessedcourses.module'; @@ -217,6 +218,7 @@ export const CORE_PROVIDERS: any[] = [ AddonBlockActivityModulesModule, AddonBlockCalendarMonthModule, AddonBlockCalendarUpcomingModule, + AddonBlockPrivateFilesModule, AddonBlockMyOverviewModule, AddonBlockSiteMainMenuModule, AddonBlockTimelineModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index d59c8689523..93cf844634d 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -40,6 +40,7 @@ "addon.block_myoverview.past": "Past", "addon.block_myoverview.pluginname": "Course overview", "addon.block_myoverview.title": "Course name", + "addon.block_privatefiles.pluginname": "Private files", "addon.block_recentlyaccessedcourses.nocourses": "No recent courses", "addon.block_recentlyaccessedcourses.pluginname": "Recently accessed courses", "addon.block_recentlyaccesseditems.noitems": "No recent items", From 29537504d01c8149b3f9b8173e9ea7b8dfb9303e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 8 May 2019 11:33:53 +0200 Subject: [PATCH 022/241] MOBILE-3014 block: Add Learning plans block feature --- scripts/langindex.json | 1 + src/addon/block/learningplans/lang/en.json | 3 ++ .../learningplans/learningplans.module.ts | 40 +++++++++++++++ .../learningplans/providers/block-handler.ts | 51 +++++++++++++++++++ src/app/app.module.ts | 4 +- src/assets/lang/en.json | 1 + 6 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 src/addon/block/learningplans/lang/en.json create mode 100644 src/addon/block/learningplans/learningplans.module.ts create mode 100644 src/addon/block/learningplans/providers/block-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index b286cb6b579..84401eb7ffa 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -29,6 +29,7 @@ "addon.block_activitymodules.pluginname": "block_activity_modules", "addon.block_calendarmonth.pluginname": "block_calendar_month", "addon.block_calendarupcoming.pluginname": "block_calendar_upcoming", + "addon.block_learningplans.pluginname": "block_lp", "addon.block_myoverview.all": "block_myoverview", "addon.block_myoverview.favourites": "block_myoverview", "addon.block_myoverview.future": "block_myoverview", diff --git a/src/addon/block/learningplans/lang/en.json b/src/addon/block/learningplans/lang/en.json new file mode 100644 index 00000000000..0a7f81e2282 --- /dev/null +++ b/src/addon/block/learningplans/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Learning plans" +} \ No newline at end of file diff --git a/src/addon/block/learningplans/learningplans.module.ts b/src/addon/block/learningplans/learningplans.module.ts new file mode 100644 index 00000000000..167178d7014 --- /dev/null +++ b/src/addon/block/learningplans/learningplans.module.ts @@ -0,0 +1,40 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockLearningPlansHandler } from './providers/block-handler'; +import { CoreBlockComponentsModule } from '@core/block/components/components.module'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + CoreBlockComponentsModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockLearningPlansHandler + ] +}) +export class AddonBlockLearningPlansModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockLearningPlansHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/learningplans/providers/block-handler.ts b/src/addon/block/learningplans/providers/block-handler.ts new file mode 100644 index 00000000000..fa98be63855 --- /dev/null +++ b/src/addon/block/learningplans/providers/block-handler.ts @@ -0,0 +1,51 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockLearningPlansHandler extends CoreBlockBaseHandler { + name = 'AddonBlockLearningPlans'; + blockName = 'lp'; + + constructor() { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + + return { + title: 'addon.block_learningplans.pluginname', + class: 'addon-block-learning-plans', + component: CoreBlockOnlyTitleComponent, + link: 'AddonCompetencyPlanListPage' + }; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 8fd2e06d131..6203c9b388f 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -94,6 +94,7 @@ import { AddonBlockActivityModulesModule } from '@addon/block/activitymodules/ac import { AddonBlockCalendarMonthModule } from '@addon/block/calendarmonth/calendarmonth.module'; import { AddonBlockCalendarUpcomingModule } from '@addon/block/calendarupcoming/calendarupcoming.module'; import { AddonBlockMyOverviewModule } from '@addon/block/myoverview/myoverview.module'; +import { AddonBlockLearningPlansModule } from '@addon/block/learningplans/learningplans.module'; import { AddonBlockPrivateFilesModule } from '@addon/block/privatefiles/privatefiles.module'; import { AddonBlockSiteMainMenuModule } from '@addon/block/sitemainmenu/sitemainmenu.module'; import { AddonBlockTimelineModule } from '@addon/block/timeline/timeline.module'; @@ -218,8 +219,9 @@ export const CORE_PROVIDERS: any[] = [ AddonBlockActivityModulesModule, AddonBlockCalendarMonthModule, AddonBlockCalendarUpcomingModule, - AddonBlockPrivateFilesModule, + AddonBlockLearningPlansModule, AddonBlockMyOverviewModule, + AddonBlockPrivateFilesModule, AddonBlockSiteMainMenuModule, AddonBlockTimelineModule, AddonBlockRecentlyAccessedCoursesModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 93cf844634d..0ac3f20453e 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -29,6 +29,7 @@ "addon.block_activitymodules.pluginname": "Activities", "addon.block_calendarmonth.pluginname": "Calendar", "addon.block_calendarupcoming.pluginname": " Upcoming events", + "addon.block_learningplans.pluginname": "Learning plans", "addon.block_myoverview.all": "All", "addon.block_myoverview.favourites": "Starred", "addon.block_myoverview.future": "Future", From c20a158525627e268e15482a725e5c5f05af4dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 8 May 2019 12:22:37 +0200 Subject: [PATCH 023/241] MOBILE-3014 block: Add Course completion status block feature --- scripts/langindex.json | 1 + .../completionstatus.module.ts | 38 ++++++++++++++ src/addon/block/completionstatus/lang/en.json | 3 ++ .../providers/block-handler.ts | 52 +++++++++++++++++++ src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + 6 files changed, 97 insertions(+) create mode 100644 src/addon/block/completionstatus/completionstatus.module.ts create mode 100644 src/addon/block/completionstatus/lang/en.json create mode 100644 src/addon/block/completionstatus/providers/block-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 84401eb7ffa..3326099140c 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -29,6 +29,7 @@ "addon.block_activitymodules.pluginname": "block_activity_modules", "addon.block_calendarmonth.pluginname": "block_calendar_month", "addon.block_calendarupcoming.pluginname": "block_calendar_upcoming", + "addon.block_completionstatus.pluginname": "block_completionstatus", "addon.block_learningplans.pluginname": "block_lp", "addon.block_myoverview.all": "block_myoverview", "addon.block_myoverview.favourites": "block_myoverview", diff --git a/src/addon/block/completionstatus/completionstatus.module.ts b/src/addon/block/completionstatus/completionstatus.module.ts new file mode 100644 index 00000000000..37d448a93b2 --- /dev/null +++ b/src/addon/block/completionstatus/completionstatus.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockCompletionStatusHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockCompletionStatusHandler + ] +}) +export class AddonBlockCompletionStatusModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockCompletionStatusHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/completionstatus/lang/en.json b/src/addon/block/completionstatus/lang/en.json new file mode 100644 index 00000000000..fe57356daba --- /dev/null +++ b/src/addon/block/completionstatus/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Course completion status" +} \ No newline at end of file diff --git a/src/addon/block/completionstatus/providers/block-handler.ts b/src/addon/block/completionstatus/providers/block-handler.ts new file mode 100644 index 00000000000..88fca4ec859 --- /dev/null +++ b/src/addon/block/completionstatus/providers/block-handler.ts @@ -0,0 +1,52 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockCompletionStatusHandler extends CoreBlockBaseHandler { + name = 'AddonBlockCompletionStatus'; + blockName = 'completionstatus'; + + constructor() { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + + return { + title: 'addon.block_completionstatus.pluginname', + class: 'addon-block-completion-status', + component: CoreBlockOnlyTitleComponent, + link: 'AddonCourseCompletionReportPage', + linkParams: { courseId: instanceId } + }; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6203c9b388f..0244cbc2ca1 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -93,6 +93,7 @@ import { AddonFilesModule } from '@addon/files/files.module'; import { AddonBlockActivityModulesModule } from '@addon/block/activitymodules/activitymodules.module'; import { AddonBlockCalendarMonthModule } from '@addon/block/calendarmonth/calendarmonth.module'; import { AddonBlockCalendarUpcomingModule } from '@addon/block/calendarupcoming/calendarupcoming.module'; +import { AddonBlockCompletionStatusModule } from '@addon/block/completionstatus/completionstatus.module'; import { AddonBlockMyOverviewModule } from '@addon/block/myoverview/myoverview.module'; import { AddonBlockLearningPlansModule } from '@addon/block/learningplans/learningplans.module'; import { AddonBlockPrivateFilesModule } from '@addon/block/privatefiles/privatefiles.module'; @@ -219,6 +220,7 @@ export const CORE_PROVIDERS: any[] = [ AddonBlockActivityModulesModule, AddonBlockCalendarMonthModule, AddonBlockCalendarUpcomingModule, + AddonBlockCompletionStatusModule, AddonBlockLearningPlansModule, AddonBlockMyOverviewModule, AddonBlockPrivateFilesModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 0ac3f20453e..d6b15d48cd8 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -29,6 +29,7 @@ "addon.block_activitymodules.pluginname": "Activities", "addon.block_calendarmonth.pluginname": "Calendar", "addon.block_calendarupcoming.pluginname": " Upcoming events", + "addon.block_completionstatus.pluginname": "Course completion status", "addon.block_learningplans.pluginname": "Learning plans", "addon.block_myoverview.all": "All", "addon.block_myoverview.favourites": "Starred", From 1c82edce2f558a2f557672c1013a23b71b1dc72e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 8 May 2019 12:35:21 +0200 Subject: [PATCH 024/241] MOBILE-3014 block: Add Self completion block feature --- scripts/langindex.json | 1 + src/addon/block/selfcompletion/lang/en.json | 3 ++ .../selfcompletion/providers/block-handler.ts | 52 +++++++++++++++++++ .../selfcompletion/selfcompletion.module.ts | 38 ++++++++++++++ src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + 6 files changed, 97 insertions(+) create mode 100644 src/addon/block/selfcompletion/lang/en.json create mode 100644 src/addon/block/selfcompletion/providers/block-handler.ts create mode 100644 src/addon/block/selfcompletion/selfcompletion.module.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 3326099140c..34a760445ad 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -47,6 +47,7 @@ "addon.block_recentlyaccessedcourses.pluginname": "block_recentlyaccessedcourses", "addon.block_recentlyaccesseditems.noitems": "block_recentlyaccesseditems", "addon.block_recentlyaccesseditems.pluginname": "block_recentlyaccesseditems", + "addon.block_selfcompletion.pluginname": "block_selfcompletion", "addon.block_sitemainmenu.pluginname": "block_site_main_menu", "addon.block_starredcourses.nocourses": "block_starredcourses", "addon.block_starredcourses.pluginname": "block_starredcourses", diff --git a/src/addon/block/selfcompletion/lang/en.json b/src/addon/block/selfcompletion/lang/en.json new file mode 100644 index 00000000000..32521695a4b --- /dev/null +++ b/src/addon/block/selfcompletion/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Self completion" +} \ No newline at end of file diff --git a/src/addon/block/selfcompletion/providers/block-handler.ts b/src/addon/block/selfcompletion/providers/block-handler.ts new file mode 100644 index 00000000000..57d716e5a41 --- /dev/null +++ b/src/addon/block/selfcompletion/providers/block-handler.ts @@ -0,0 +1,52 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockSelfCompletionHandler extends CoreBlockBaseHandler { + name = 'AddonBlockSelfCompletion'; + blockName = 'selfcompletion'; + + constructor() { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + + return { + title: 'addon.block_selfcompletion.pluginname', + class: 'addon-block-self-completion', + component: CoreBlockOnlyTitleComponent, + link: 'AddonCourseCompletionReportPage', + linkParams: { courseId: instanceId } + }; + } +} diff --git a/src/addon/block/selfcompletion/selfcompletion.module.ts b/src/addon/block/selfcompletion/selfcompletion.module.ts new file mode 100644 index 00000000000..b101210b2d7 --- /dev/null +++ b/src/addon/block/selfcompletion/selfcompletion.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockSelfCompletionHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockSelfCompletionHandler + ] +}) +export class AddonBlockSelfCompletionModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockSelfCompletionHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0244cbc2ca1..594416b756d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -102,6 +102,7 @@ import { AddonBlockTimelineModule } from '@addon/block/timeline/timeline.module' import { AddonBlockRecentlyAccessedCoursesModule } from '@addon/block/recentlyaccessedcourses/recentlyaccessedcourses.module'; import { AddonBlockRecentlyAccessedItemsModule } from '@addon/block/recentlyaccesseditems/recentlyaccesseditems.module'; import { AddonBlockStarredCoursesModule } from '@addon/block/starredcourses/starredcourses.module'; +import { AddonBlockSelfCompletionModule } from '@addon/block/selfcompletion/selfcompletion.module'; import { AddonModAssignModule } from '@addon/mod/assign/assign.module'; import { AddonModBookModule } from '@addon/mod/book/book.module'; import { AddonModChatModule } from '@addon/mod/chat/chat.module'; @@ -229,6 +230,7 @@ export const CORE_PROVIDERS: any[] = [ AddonBlockRecentlyAccessedCoursesModule, AddonBlockRecentlyAccessedItemsModule, AddonBlockStarredCoursesModule, + AddonBlockSelfCompletionModule, AddonModAssignModule, AddonModBookModule, AddonModChatModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index d6b15d48cd8..8bc1efe9bbc 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -47,6 +47,7 @@ "addon.block_recentlyaccessedcourses.pluginname": "Recently accessed courses", "addon.block_recentlyaccesseditems.noitems": "No recent items", "addon.block_recentlyaccesseditems.pluginname": "Recently accessed items", + "addon.block_selfcompletion.pluginname": "Self completion", "addon.block_sitemainmenu.pluginname": "Main menu", "addon.block_starredcourses.nocourses": "No starred courses", "addon.block_starredcourses.pluginname": "Starred courses", From f368333ca148bf796c9ab698fc9860c3372bc87c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 8 May 2019 12:55:22 +0200 Subject: [PATCH 025/241] MOBILE-3014 block: Add Comments block feature --- scripts/langindex.json | 1 + src/addon/block/comments/comments.module.ts | 38 +++++++++++++ src/addon/block/comments/lang/en.json | 3 ++ .../block/comments/providers/block-handler.ts | 53 +++++++++++++++++++ src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + 6 files changed, 98 insertions(+) create mode 100644 src/addon/block/comments/comments.module.ts create mode 100644 src/addon/block/comments/lang/en.json create mode 100644 src/addon/block/comments/providers/block-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 34a760445ad..1d142fd09c9 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -29,6 +29,7 @@ "addon.block_activitymodules.pluginname": "block_activity_modules", "addon.block_calendarmonth.pluginname": "block_calendar_month", "addon.block_calendarupcoming.pluginname": "block_calendar_upcoming", + "addon.block_comments.pluginname": "block_comments", "addon.block_completionstatus.pluginname": "block_completionstatus", "addon.block_learningplans.pluginname": "block_lp", "addon.block_myoverview.all": "block_myoverview", diff --git a/src/addon/block/comments/comments.module.ts b/src/addon/block/comments/comments.module.ts new file mode 100644 index 00000000000..dce565e7da6 --- /dev/null +++ b/src/addon/block/comments/comments.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockCommentsHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockCommentsHandler + ] +}) +export class AddonBlockCommentsModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockCommentsHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/comments/lang/en.json b/src/addon/block/comments/lang/en.json new file mode 100644 index 00000000000..adcbcabae8f --- /dev/null +++ b/src/addon/block/comments/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Comments" +} \ No newline at end of file diff --git a/src/addon/block/comments/providers/block-handler.ts b/src/addon/block/comments/providers/block-handler.ts new file mode 100644 index 00000000000..81e5b2c15e7 --- /dev/null +++ b/src/addon/block/comments/providers/block-handler.ts @@ -0,0 +1,53 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockCommentsHandler extends CoreBlockBaseHandler { + name = 'AddonBlockComments'; + blockName = 'comments'; + + constructor() { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + + return { + title: 'addon.block_comments.pluginname', + class: 'addon-block-comments', + component: CoreBlockOnlyTitleComponent, + link: 'CoreCommentsViewerPage', + linkParams: { contextLevel: contextLevel, instanceId: instanceId, + component: 'block_comments', area: 'page_comments', itemId: 0 } + }; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 594416b756d..3086286a155 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -93,6 +93,7 @@ import { AddonFilesModule } from '@addon/files/files.module'; import { AddonBlockActivityModulesModule } from '@addon/block/activitymodules/activitymodules.module'; import { AddonBlockCalendarMonthModule } from '@addon/block/calendarmonth/calendarmonth.module'; import { AddonBlockCalendarUpcomingModule } from '@addon/block/calendarupcoming/calendarupcoming.module'; +import { AddonBlockCommentsModule } from '@addon/block/comments/comments.module'; import { AddonBlockCompletionStatusModule } from '@addon/block/completionstatus/completionstatus.module'; import { AddonBlockMyOverviewModule } from '@addon/block/myoverview/myoverview.module'; import { AddonBlockLearningPlansModule } from '@addon/block/learningplans/learningplans.module'; @@ -221,6 +222,7 @@ export const CORE_PROVIDERS: any[] = [ AddonBlockActivityModulesModule, AddonBlockCalendarMonthModule, AddonBlockCalendarUpcomingModule, + AddonBlockCommentsModule, AddonBlockCompletionStatusModule, AddonBlockLearningPlansModule, AddonBlockMyOverviewModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 8bc1efe9bbc..c28b1240e97 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -29,6 +29,7 @@ "addon.block_activitymodules.pluginname": "Activities", "addon.block_calendarmonth.pluginname": "Calendar", "addon.block_calendarupcoming.pluginname": " Upcoming events", + "addon.block_comments.pluginname": "Comments", "addon.block_completionstatus.pluginname": "Course completion status", "addon.block_learningplans.pluginname": "Learning plans", "addon.block_myoverview.all": "All", From 34061ad5abb76c2a06211a05e229e27802ecd97c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 9 May 2019 14:25:27 +0200 Subject: [PATCH 026/241] MOBILE-3002 block: Support for pre rendered block type --- .../block/components/components.module.ts | 4 ++ .../core-block-pre-rendered.html | 11 +++++ .../pre-rendered-block/pre-rendered-block.ts | 40 +++++++++++++++++++ .../block/providers/course-option-handler.ts | 4 +- src/core/course/providers/course.ts | 3 +- src/core/courses/providers/dashboard.ts | 1 + 6 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 src/core/block/components/pre-rendered-block/core-block-pre-rendered.html create mode 100644 src/core/block/components/pre-rendered-block/pre-rendered-block.ts diff --git a/src/core/block/components/components.module.ts b/src/core/block/components/components.module.ts index 33c80abcff0..70627f6bfd0 100644 --- a/src/core/block/components/components.module.ts +++ b/src/core/block/components/components.module.ts @@ -19,6 +19,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreBlockComponent } from './block/block'; import { CoreBlockOnlyTitleComponent } from './only-title-block/only-title-block'; +import { CoreBlockPreRenderedComponent } from './pre-rendered-block/pre-rendered-block'; import { CoreBlockCourseBlocksComponent } from './course-blocks/course-blocks'; import { CoreComponentsModule } from '@components/components.module'; @@ -26,6 +27,7 @@ import { CoreComponentsModule } from '@components/components.module'; declarations: [ CoreBlockComponent, CoreBlockOnlyTitleComponent, + CoreBlockPreRenderedComponent, CoreBlockCourseBlocksComponent ], imports: [ @@ -40,10 +42,12 @@ import { CoreComponentsModule } from '@components/components.module'; exports: [ CoreBlockComponent, CoreBlockOnlyTitleComponent, + CoreBlockPreRenderedComponent, CoreBlockCourseBlocksComponent ], entryComponents: [ CoreBlockOnlyTitleComponent, + CoreBlockPreRenderedComponent, CoreBlockCourseBlocksComponent ] }) diff --git a/src/core/block/components/pre-rendered-block/core-block-pre-rendered.html b/src/core/block/components/pre-rendered-block/core-block-pre-rendered.html new file mode 100644 index 00000000000..84780cabb5e --- /dev/null +++ b/src/core/block/components/pre-rendered-block/core-block-pre-rendered.html @@ -0,0 +1,11 @@ + +

+
+ + + + + + + + diff --git a/src/core/block/components/pre-rendered-block/pre-rendered-block.ts b/src/core/block/components/pre-rendered-block/pre-rendered-block.ts new file mode 100644 index 00000000000..0acd1712f53 --- /dev/null +++ b/src/core/block/components/pre-rendered-block/pre-rendered-block.ts @@ -0,0 +1,40 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Injector, OnInit, Component } from '@angular/core'; +import { CoreBlockBaseComponent } from '../../classes/base-block-component'; + +/** + * Component to render blocks with pre-rendered HTML. + */ +@Component({ + selector: 'core-block-pre-rendered', + templateUrl: 'core-block-pre-rendered.html' +}) +export class CoreBlockPreRenderedComponent extends CoreBlockBaseComponent implements OnInit { + + constructor(injector: Injector) { + super(injector, 'CoreBlockPreRenderedComponent'); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + super.ngOnInit(); + + this.fetchContentDefaultError = 'Error getting ' + this.block.contents.title + ' data.'; + } + +} diff --git a/src/core/block/providers/course-option-handler.ts b/src/core/block/providers/course-option-handler.ts index c7e298c9cb8..7e38227f524 100644 --- a/src/core/block/providers/course-option-handler.ts +++ b/src/core/block/providers/course-option-handler.ts @@ -58,7 +58,9 @@ export class CoreBlockCourseBlocksCourseOptionHandler implements CoreCourseOptio * @return {boolean|Promise} True or promise resolved with true if enabled. */ isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise { - return true; + return this.courseProvider.getCourseBlocks(courseId).then((blocks) => { + return blocks && blocks.length > 0; + }); } /** diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index bb4a55c5a89..3131fc8699b 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -255,7 +255,8 @@ export class CoreCourseProvider { getCourseBlocks(courseId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { - courseid: courseId + courseid: courseId, + returncontents: 1 }, preSets: CoreSiteWSPreSets = { cacheKey: this.getCourseBlocksCacheKey(courseId), diff --git a/src/core/courses/providers/dashboard.ts b/src/core/courses/providers/dashboard.ts index 24ac80e2c11..beb6204d06b 100644 --- a/src/core/courses/providers/dashboard.ts +++ b/src/core/courses/providers/dashboard.ts @@ -47,6 +47,7 @@ export class CoreCoursesDashboardProvider { getDashboardBlocks(userId?: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { + returncontents: 1 }, preSets = { cacheKey: this.getDashboardBlocksCacheKey(userId), From 11eee9b8a8bec67eb26fddd30174457b60dfbbda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 7 May 2019 12:39:55 +0200 Subject: [PATCH 027/241] MOBILE-3002 block: Add HTML block feature --- src/addon/block/html/html.module.ts | 36 +++++++++++++ .../block/html/providers/block-handler.ts | 50 +++++++++++++++++++ src/app/app.module.ts | 2 + src/theme/format-text.scss | 3 +- 4 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 src/addon/block/html/html.module.ts create mode 100644 src/addon/block/html/providers/block-handler.ts diff --git a/src/addon/block/html/html.module.ts b/src/addon/block/html/html.module.ts new file mode 100644 index 00000000000..1b81cf4adb1 --- /dev/null +++ b/src/addon/block/html/html.module.ts @@ -0,0 +1,36 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockHtmlHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule + ], + exports: [ + ], + providers: [ + AddonBlockHtmlHandler + ] +}) +export class AddonBlockHtmlModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockHtmlHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/html/providers/block-handler.ts b/src/addon/block/html/providers/block-handler.ts new file mode 100644 index 00000000000..a5603fb53fc --- /dev/null +++ b/src/addon/block/html/providers/block-handler.ts @@ -0,0 +1,50 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockHtmlHandler extends CoreBlockBaseHandler { + name = 'AddonBlockHtml'; + blockName = 'html'; + + constructor() { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + + return { + title: block.contents.title, + class: 'addon-block-html', + component: CoreBlockPreRenderedComponent + }; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3086286a155..fa19ec00885 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -95,6 +95,7 @@ import { AddonBlockCalendarMonthModule } from '@addon/block/calendarmonth/calend import { AddonBlockCalendarUpcomingModule } from '@addon/block/calendarupcoming/calendarupcoming.module'; import { AddonBlockCommentsModule } from '@addon/block/comments/comments.module'; import { AddonBlockCompletionStatusModule } from '@addon/block/completionstatus/completionstatus.module'; +import { AddonBlockHtmlModule } from '@addon/block/html/html.module'; import { AddonBlockMyOverviewModule } from '@addon/block/myoverview/myoverview.module'; import { AddonBlockLearningPlansModule } from '@addon/block/learningplans/learningplans.module'; import { AddonBlockPrivateFilesModule } from '@addon/block/privatefiles/privatefiles.module'; @@ -224,6 +225,7 @@ export const CORE_PROVIDERS: any[] = [ AddonBlockCalendarUpcomingModule, AddonBlockCommentsModule, AddonBlockCompletionStatusModule, + AddonBlockHtmlModule, AddonBlockLearningPlansModule, AddonBlockMyOverviewModule, AddonBlockPrivateFilesModule, diff --git a/src/theme/format-text.scss b/src/theme/format-text.scss index ad0a230fb4c..d14efd14877 100644 --- a/src/theme/format-text.scss +++ b/src/theme/format-text.scss @@ -4,9 +4,8 @@ ion-app.app-root .item core-format-text, ion-app.app-root core-rich-text-editor .core-rte-editor { @include core-headings(); - font-size: 1.4rem; - p { + font-size: 1.4rem; margin-bottom: 1rem; } From 16d768d1876d48bdb8e99e4fa261261b476905af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 9 May 2019 14:31:08 +0200 Subject: [PATCH 028/241] MOBILE-3002 block: Add Online users block feature --- scripts/langindex.json | 1 + src/addon/block/onlineusers/lang/en.json | 3 ++ .../block/onlineusers/onlineusers.module.ts | 38 ++++++++++++++ src/addon/block/onlineusers/onlineusers.scss | 40 +++++++++++++++ .../onlineusers/providers/block-handler.ts | 50 +++++++++++++++++++ src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + 7 files changed, 135 insertions(+) create mode 100644 src/addon/block/onlineusers/lang/en.json create mode 100644 src/addon/block/onlineusers/onlineusers.module.ts create mode 100644 src/addon/block/onlineusers/onlineusers.scss create mode 100644 src/addon/block/onlineusers/providers/block-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 1d142fd09c9..a700c40cd79 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -43,6 +43,7 @@ "addon.block_myoverview.past": "block_myoverview", "addon.block_myoverview.pluginname": "block_myoverview", "addon.block_myoverview.title": "block_myoverview", + "addon.block_onlineusers.pluginname": "block_online_users", "addon.block_privatefiles.pluginname": "block_private_files", "addon.block_recentlyaccessedcourses.nocourses": "block_recentlyaccessedcourses", "addon.block_recentlyaccessedcourses.pluginname": "block_recentlyaccessedcourses", diff --git a/src/addon/block/onlineusers/lang/en.json b/src/addon/block/onlineusers/lang/en.json new file mode 100644 index 00000000000..4bc6cd41263 --- /dev/null +++ b/src/addon/block/onlineusers/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Online users" +} \ No newline at end of file diff --git a/src/addon/block/onlineusers/onlineusers.module.ts b/src/addon/block/onlineusers/onlineusers.module.ts new file mode 100644 index 00000000000..0df61ad7a1f --- /dev/null +++ b/src/addon/block/onlineusers/onlineusers.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockOnlineUsersHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockOnlineUsersHandler + ] +}) +export class AddonBlockOnlineUsersModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockOnlineUsersHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/onlineusers/onlineusers.scss b/src/addon/block/onlineusers/onlineusers.scss new file mode 100644 index 00000000000..eb05cef2edf --- /dev/null +++ b/src/addon/block/onlineusers/onlineusers.scss @@ -0,0 +1,40 @@ +.addon-block-online-users core-block-pre-rendered .core-block-content { + .list { + @include margin-horizontal(0); + -webkit-padding-start: 0; + + li.listentry { + clear: both; + list-style-type: none; + + .user { + @include float(start); + position: relative; + padding-bottom: 16px; + + .core-adapted-img-container { + display: inline; + @include margin-horizontal(0, 8px); + } + + .userpicture { + vertical-align: text-bottom; + } + } + + .message { + @include float(end); + margin-top: 3px; + } + + .uservisibility { // No support on the app. + display: none; + } + } + } + + .info { + text-align: center; + } + +} \ No newline at end of file diff --git a/src/addon/block/onlineusers/providers/block-handler.ts b/src/addon/block/onlineusers/providers/block-handler.ts new file mode 100644 index 00000000000..9967ad6f679 --- /dev/null +++ b/src/addon/block/onlineusers/providers/block-handler.ts @@ -0,0 +1,50 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockOnlineUsersHandler extends CoreBlockBaseHandler { + name = 'AddonBlockOnlineUsers'; + blockName = 'online_users'; + + constructor(private translate: TranslateService) { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + return { + title: this.translate.instant('addon.block_onlineusers.pluginname'), + class: 'addon-block-online-users', + component: CoreBlockPreRenderedComponent + }; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index fa19ec00885..8ce3a17964c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -97,6 +97,7 @@ import { AddonBlockCommentsModule } from '@addon/block/comments/comments.module' import { AddonBlockCompletionStatusModule } from '@addon/block/completionstatus/completionstatus.module'; import { AddonBlockHtmlModule } from '@addon/block/html/html.module'; import { AddonBlockMyOverviewModule } from '@addon/block/myoverview/myoverview.module'; +import { AddonBlockOnlineUsersModule } from '@addon/block/onlineusers/onlineusers.module'; import { AddonBlockLearningPlansModule } from '@addon/block/learningplans/learningplans.module'; import { AddonBlockPrivateFilesModule } from '@addon/block/privatefiles/privatefiles.module'; import { AddonBlockSiteMainMenuModule } from '@addon/block/sitemainmenu/sitemainmenu.module'; @@ -228,6 +229,7 @@ export const CORE_PROVIDERS: any[] = [ AddonBlockHtmlModule, AddonBlockLearningPlansModule, AddonBlockMyOverviewModule, + AddonBlockOnlineUsersModule, AddonBlockPrivateFilesModule, AddonBlockSiteMainMenuModule, AddonBlockTimelineModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index c28b1240e97..4d3f6b00e9d 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -43,6 +43,7 @@ "addon.block_myoverview.past": "Past", "addon.block_myoverview.pluginname": "Course overview", "addon.block_myoverview.title": "Course name", + "addon.block_onlineusers.pluginname": "Online users", "addon.block_privatefiles.pluginname": "Private files", "addon.block_recentlyaccessedcourses.nocourses": "No recent courses", "addon.block_recentlyaccessedcourses.pluginname": "Recently accessed courses", From 67be0a6c45640d97076f8aaea5f8c0c7c7568eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 10 May 2019 11:53:26 +0200 Subject: [PATCH 029/241] MOBILE-3002 block: Add Latest announcements block feature --- scripts/langindex.json | 1 + src/addon/block/newsitems/lang/en.json | 3 + src/addon/block/newsitems/newsitems.module.ts | 38 +++++++++ src/addon/block/newsitems/newsitems.scss | 26 ++++++ .../newsitems/providers/block-handler.ts | 50 +++++++++++ src/addon/mod/forum/forum.module.ts | 6 +- .../mod/forum/pages/discussion/discussion.ts | 3 + .../mod/forum/providers/index-link-handler.ts | 40 ++++++++- .../mod/forum/providers/post-link-handler.ts | 84 +++++++++++++++++++ src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + 11 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 src/addon/block/newsitems/lang/en.json create mode 100644 src/addon/block/newsitems/newsitems.module.ts create mode 100644 src/addon/block/newsitems/newsitems.scss create mode 100644 src/addon/block/newsitems/providers/block-handler.ts create mode 100644 src/addon/mod/forum/providers/post-link-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index a700c40cd79..20feed1fe00 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -43,6 +43,7 @@ "addon.block_myoverview.past": "block_myoverview", "addon.block_myoverview.pluginname": "block_myoverview", "addon.block_myoverview.title": "block_myoverview", + "addon.block_newsitems.pluginname": "block_news_items", "addon.block_onlineusers.pluginname": "block_online_users", "addon.block_privatefiles.pluginname": "block_private_files", "addon.block_recentlyaccessedcourses.nocourses": "block_recentlyaccessedcourses", diff --git a/src/addon/block/newsitems/lang/en.json b/src/addon/block/newsitems/lang/en.json new file mode 100644 index 00000000000..83b98129739 --- /dev/null +++ b/src/addon/block/newsitems/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Latest announcements" +} \ No newline at end of file diff --git a/src/addon/block/newsitems/newsitems.module.ts b/src/addon/block/newsitems/newsitems.module.ts new file mode 100644 index 00000000000..a3364ea5ea0 --- /dev/null +++ b/src/addon/block/newsitems/newsitems.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockNewsItemsHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockNewsItemsHandler + ] +}) +export class AddonBlockNewsItemsModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockNewsItemsHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/newsitems/newsitems.scss b/src/addon/block/newsitems/newsitems.scss new file mode 100644 index 00000000000..8b0c46287b9 --- /dev/null +++ b/src/addon/block/newsitems/newsitems.scss @@ -0,0 +1,26 @@ +.addon-block-news-items core-block-pre-rendered { + .core-block-content { + .unlist { + list-style-type: none; + @include margin-horizontal(0); + -webkit-padding-start: 0; + + li.post { + padding-bottom: 16px; + } + li.post:last-child { + padding-bottom: 0; + } + } + } + + // Hide RSS link. + .core-block-footer { + a { + display: none; + } + a:first-child { + display: inline; + } + } +} \ No newline at end of file diff --git a/src/addon/block/newsitems/providers/block-handler.ts b/src/addon/block/newsitems/providers/block-handler.ts new file mode 100644 index 00000000000..9b6c15cb831 --- /dev/null +++ b/src/addon/block/newsitems/providers/block-handler.ts @@ -0,0 +1,50 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockNewsItemsHandler extends CoreBlockBaseHandler { + name = 'AddonBlockNewsItems'; + blockName = 'news_items'; + + constructor(private translate: TranslateService) { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + return { + title: this.translate.instant('addon.block_newsitems.pluginname'), + class: 'addon-block-news-items', + component: CoreBlockPreRenderedComponent + }; + } +} diff --git a/src/addon/mod/forum/forum.module.ts b/src/addon/mod/forum/forum.module.ts index 215c343e647..94fe30257c0 100644 --- a/src/addon/mod/forum/forum.module.ts +++ b/src/addon/mod/forum/forum.module.ts @@ -28,6 +28,7 @@ import { AddonModForumSyncCronHandler } from './providers/sync-cron-handler'; import { AddonModForumIndexLinkHandler } from './providers/index-link-handler'; import { AddonModForumDiscussionLinkHandler } from './providers/discussion-link-handler'; import { AddonModForumListLinkHandler } from './providers/list-link-handler'; +import { AddonModForumPostLinkHandler } from './providers/post-link-handler'; import { AddonModForumPushClickHandler } from './providers/push-click-handler'; import { AddonModForumComponentsModule } from './components/components.module'; import { CoreUpdateManagerProvider } from '@providers/update-manager'; @@ -56,6 +57,7 @@ export const ADDON_MOD_FORUM_PROVIDERS: any[] = [ AddonModForumSyncCronHandler, AddonModForumIndexLinkHandler, AddonModForumListLinkHandler, + AddonModForumPostLinkHandler, AddonModForumDiscussionLinkHandler, AddonModForumPushClickHandler ] @@ -66,7 +68,8 @@ export class AddonModForumModule { cronDelegate: CoreCronDelegate, syncHandler: AddonModForumSyncCronHandler, linksDelegate: CoreContentLinksDelegate, indexHandler: AddonModForumIndexLinkHandler, discussionHandler: AddonModForumDiscussionLinkHandler, updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModForumListLinkHandler, - pushNotificationsDelegate: CorePushNotificationsDelegate, pushClickHandler: AddonModForumPushClickHandler) { + pushNotificationsDelegate: CorePushNotificationsDelegate, pushClickHandler: AddonModForumPushClickHandler, + postLinkHandler: AddonModForumPostLinkHandler) { moduleDelegate.registerHandler(moduleHandler); prefetchDelegate.registerHandler(prefetchHandler); @@ -74,6 +77,7 @@ export class AddonModForumModule { linksDelegate.registerHandler(indexHandler); linksDelegate.registerHandler(discussionHandler); linksDelegate.registerHandler(listLinkHandler); + linksDelegate.registerHandler(postLinkHandler); pushNotificationsDelegate.registerClickHandler(pushClickHandler); // Allow migrating the tables from the old app to the new schema. diff --git a/src/addon/mod/forum/pages/discussion/discussion.ts b/src/addon/mod/forum/pages/discussion/discussion.ts index f8514dd5f9f..68598a18ee1 100644 --- a/src/addon/mod/forum/pages/discussion/discussion.ts +++ b/src/addon/mod/forum/pages/discussion/discussion.ts @@ -354,6 +354,9 @@ export class AddonModForumDiscussionPage implements OnDestroy { return Promise.reject('Invalid forum discussion.'); } + this.defaultSubject = this.translate.instant('addon.mod_forum.re') + ' ' + this.discussion.subject; + this.replyData.subject = this.defaultSubject; + if (this.discussion.userfullname && this.discussion.parent == 0 && this.forum.type == 'single') { // Hide author for first post and type single. this.discussion.userfullname = null; diff --git a/src/addon/mod/forum/providers/index-link-handler.ts b/src/addon/mod/forum/providers/index-link-handler.ts index 4beb1226f9c..b975f449c7f 100644 --- a/src/addon/mod/forum/providers/index-link-handler.ts +++ b/src/addon/mod/forum/providers/index-link-handler.ts @@ -16,6 +16,9 @@ import { Injectable } from '@angular/core'; import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { AddonModForumProvider } from './forum'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; /** * Handler to treat links to forum index. @@ -24,8 +27,12 @@ import { AddonModForumProvider } from './forum'; export class AddonModForumIndexLinkHandler extends CoreContentLinksModuleIndexHandler { name = 'AddonModForumIndexLinkHandler'; - constructor(courseHelper: CoreCourseHelperProvider, protected forumProvider: AddonModForumProvider) { + constructor(courseHelper: CoreCourseHelperProvider, protected forumProvider: AddonModForumProvider, + private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider) { super(courseHelper, 'AddonModForum', 'forum'); + + // Match the view.php URL with an id param. + this.pattern = new RegExp('\/mod\/forum\/view\.php.*([\&\?](f|id)=\\d+)'); } /** @@ -41,4 +48,35 @@ export class AddonModForumIndexLinkHandler extends CoreContentLinksModuleIndexHa isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { return true; } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + + if (typeof params.f != 'undefined') { + return [{ + action: (siteId, navCtrl?): void => { + const modal = this.domUtils.showModalLoading(), + forumId = parseInt(params.f, 10); + + this.courseProvider.getModuleBasicInfoByInstance(forumId, 'forum', siteId).then((module) => { + this.courseHelper.navigateToModule(parseInt(module.id, 10), siteId, module.course); + }).finally(() => { + // Just in case. In fact we need to dismiss the modal before showing a toast or error message. + modal.dismiss(); + }); + } + }]; + } + + return super.getActions(siteIds, url, params, courseId); + } } diff --git a/src/addon/mod/forum/providers/post-link-handler.ts b/src/addon/mod/forum/providers/post-link-handler.ts new file mode 100644 index 00000000000..73d691dc199 --- /dev/null +++ b/src/addon/mod/forum/providers/post-link-handler.ts @@ -0,0 +1,84 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; + +/** + * Content links handler for forum new discussion. + * Match mod/forum/post.php?forum=6 with a valid data. + */ +@Injectable() +export class AddonModForumPostLinkHandler extends CoreContentLinksHandlerBase { + name = 'AddonModForumPostLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModForum'; + pattern = /\/mod\/forum\/post\.php.*([\?\&](forum)=\d+)/; + + constructor(private linkHelper: CoreContentLinksHelperProvider, private courseProvider: CoreCourseProvider, + private domUtils: CoreDomUtilsProvider) { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + + return [{ + action: (siteId, navCtrl?): void => { + const modal = this.domUtils.showModalLoading(), + forumId = parseInt(params.forum, 10); + + this.courseProvider.getModuleBasicInfoByInstance(forumId, 'forum', siteId).then((module) => { + const pageParams = { + courseId: module.course, + cmId: module.id, + forumId: module.instance, + timeCreated: 0, + }; + + return this.linkHelper.goInSite(navCtrl, 'AddonModForumNewDiscussionPage', pageParams, siteId); + }).finally(() => { + // Just in case. In fact we need to dismiss the modal before showing a toast or error message. + modal.dismiss(); + }); + } + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + return typeof params.forum != 'undefined'; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 8ce3a17964c..4d47432947c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -97,6 +97,7 @@ import { AddonBlockCommentsModule } from '@addon/block/comments/comments.module' import { AddonBlockCompletionStatusModule } from '@addon/block/completionstatus/completionstatus.module'; import { AddonBlockHtmlModule } from '@addon/block/html/html.module'; import { AddonBlockMyOverviewModule } from '@addon/block/myoverview/myoverview.module'; +import { AddonBlockNewsItemsModule } from '@addon/block/newsitems/newsitems.module'; import { AddonBlockOnlineUsersModule } from '@addon/block/onlineusers/onlineusers.module'; import { AddonBlockLearningPlansModule } from '@addon/block/learningplans/learningplans.module'; import { AddonBlockPrivateFilesModule } from '@addon/block/privatefiles/privatefiles.module'; @@ -229,6 +230,7 @@ export const CORE_PROVIDERS: any[] = [ AddonBlockHtmlModule, AddonBlockLearningPlansModule, AddonBlockMyOverviewModule, + AddonBlockNewsItemsModule, AddonBlockOnlineUsersModule, AddonBlockPrivateFilesModule, AddonBlockSiteMainMenuModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 4d3f6b00e9d..88734646f4d 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -43,6 +43,7 @@ "addon.block_myoverview.past": "Past", "addon.block_myoverview.pluginname": "Course overview", "addon.block_myoverview.title": "Course name", + "addon.block_newsitems.pluginname": "Latest announcements", "addon.block_onlineusers.pluginname": "Online users", "addon.block_privatefiles.pluginname": "Private files", "addon.block_recentlyaccessedcourses.nocourses": "No recent courses", From 00993853956b5b8f17759bda36806f8696d06c51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 10 May 2019 15:15:07 +0200 Subject: [PATCH 030/241] MOBILE-3002 block: Add Glossary random block feature --- scripts/langindex.json | 1 + .../glossaryrandom/glossaryrandom.module.ts | 38 +++++++++ src/addon/block/glossaryrandom/lang/en.json | 3 + .../glossaryrandom/providers/block-handler.ts | 50 +++++++++++ src/addon/mod/glossary/glossary.module.ts | 8 +- .../glossary/providers/edit-link-handler.ts | 85 +++++++++++++++++++ src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + 8 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 src/addon/block/glossaryrandom/glossaryrandom.module.ts create mode 100644 src/addon/block/glossaryrandom/lang/en.json create mode 100644 src/addon/block/glossaryrandom/providers/block-handler.ts create mode 100644 src/addon/mod/glossary/providers/edit-link-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 20feed1fe00..1c36c87eafa 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -31,6 +31,7 @@ "addon.block_calendarupcoming.pluginname": "block_calendar_upcoming", "addon.block_comments.pluginname": "block_comments", "addon.block_completionstatus.pluginname": "block_completionstatus", + "addon.block_glossaryrandom.pluginname": "block_glossary_random", "addon.block_learningplans.pluginname": "block_lp", "addon.block_myoverview.all": "block_myoverview", "addon.block_myoverview.favourites": "block_myoverview", diff --git a/src/addon/block/glossaryrandom/glossaryrandom.module.ts b/src/addon/block/glossaryrandom/glossaryrandom.module.ts new file mode 100644 index 00000000000..e6d23895706 --- /dev/null +++ b/src/addon/block/glossaryrandom/glossaryrandom.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockGlossaryRandomHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockGlossaryRandomHandler + ] +}) +export class AddonBlockGlossaryRandomModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockGlossaryRandomHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/glossaryrandom/lang/en.json b/src/addon/block/glossaryrandom/lang/en.json new file mode 100644 index 00000000000..1ae4de38c6e --- /dev/null +++ b/src/addon/block/glossaryrandom/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Random glossary entry" +} \ No newline at end of file diff --git a/src/addon/block/glossaryrandom/providers/block-handler.ts b/src/addon/block/glossaryrandom/providers/block-handler.ts new file mode 100644 index 00000000000..48b97b5a1b3 --- /dev/null +++ b/src/addon/block/glossaryrandom/providers/block-handler.ts @@ -0,0 +1,50 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockGlossaryRandomHandler extends CoreBlockBaseHandler { + name = 'AddonBlockGlossaryRandom'; + blockName = 'glossary_random'; + + constructor(private translate: TranslateService) { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + return { + title: block.contents.title || this.translate.instant('addon.block_glossaryrandom.pluginname'), + class: 'addon-block-glossary-random', + component: CoreBlockPreRenderedComponent + }; + } +} diff --git a/src/addon/mod/glossary/glossary.module.ts b/src/addon/mod/glossary/glossary.module.ts index ab114c54bc1..ac311c14449 100644 --- a/src/addon/mod/glossary/glossary.module.ts +++ b/src/addon/mod/glossary/glossary.module.ts @@ -27,6 +27,7 @@ import { AddonModGlossarySyncCronHandler } from './providers/sync-cron-handler'; import { AddonModGlossaryIndexLinkHandler } from './providers/index-link-handler'; import { AddonModGlossaryEntryLinkHandler } from './providers/entry-link-handler'; import { AddonModGlossaryListLinkHandler } from './providers/list-link-handler'; +import { AddonModGlossaryEditLinkHandler } from './providers/edit-link-handler'; import { AddonModGlossaryComponentsModule } from './components/components.module'; import { CoreUpdateManagerProvider } from '@providers/update-manager'; @@ -54,7 +55,8 @@ export const ADDON_MOD_GLOSSARY_PROVIDERS: any[] = [ AddonModGlossarySyncCronHandler, AddonModGlossaryIndexLinkHandler, AddonModGlossaryEntryLinkHandler, - AddonModGlossaryListLinkHandler + AddonModGlossaryListLinkHandler, + AddonModGlossaryEditLinkHandler ] }) export class AddonModGlossaryModule { @@ -62,7 +64,8 @@ export class AddonModGlossaryModule { prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModGlossaryPrefetchHandler, cronDelegate: CoreCronDelegate, syncHandler: AddonModGlossarySyncCronHandler, linksDelegate: CoreContentLinksDelegate, indexHandler: AddonModGlossaryIndexLinkHandler, discussionHandler: AddonModGlossaryEntryLinkHandler, - updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModGlossaryListLinkHandler) { + updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModGlossaryListLinkHandler, + editLinkHandler: AddonModGlossaryEditLinkHandler) { moduleDelegate.registerHandler(moduleHandler); prefetchDelegate.registerHandler(prefetchHandler); @@ -70,6 +73,7 @@ export class AddonModGlossaryModule { linksDelegate.registerHandler(indexHandler); linksDelegate.registerHandler(discussionHandler); linksDelegate.registerHandler(listLinkHandler); + linksDelegate.registerHandler(editLinkHandler); // Allow migrating the tables from the old app to the new schema. updateManager.registerSiteTableMigration({ diff --git a/src/addon/mod/glossary/providers/edit-link-handler.ts b/src/addon/mod/glossary/providers/edit-link-handler.ts new file mode 100644 index 00000000000..c2f34305c59 --- /dev/null +++ b/src/addon/mod/glossary/providers/edit-link-handler.ts @@ -0,0 +1,85 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; + +/** + * Content links handler for glossary new entry. + * Match mod/glossary/edit.php?cmid=6 with a valid data. + * Currently it only supports new entry. + */ +@Injectable() +export class AddonModGlossaryEditLinkHandler extends CoreContentLinksHandlerBase { + name = 'AddonModGlossaryEditLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModGlossary'; + pattern = /\/mod\/glossary\/edit\.php.*([\?\&](cmid)=\d+)/; + + constructor(private linkHelper: CoreContentLinksHelperProvider, private courseProvider: CoreCourseProvider, + private domUtils: CoreDomUtilsProvider) { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + + return [{ + action: (siteId, navCtrl?): void => { + const modal = this.domUtils.showModalLoading(), + cmId = parseInt(params.cmid, 10); + + this.courseProvider.getModuleBasicInfo(cmId, siteId).then((module) => { + const pageParams = { + courseId: module.course, + module: module, + glossary: module.module, + entry: null // It does not support entry editing. + }; + + return this.linkHelper.goInSite(navCtrl, 'AddonModGlossaryEditPage', pageParams, siteId); + }).finally(() => { + // Just in case. In fact we need to dismiss the modal before showing a toast or error message. + modal.dismiss(); + }); + } + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + return typeof params.cmid != 'undefined'; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 4d47432947c..6069ee98333 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -95,6 +95,7 @@ import { AddonBlockCalendarMonthModule } from '@addon/block/calendarmonth/calend import { AddonBlockCalendarUpcomingModule } from '@addon/block/calendarupcoming/calendarupcoming.module'; import { AddonBlockCommentsModule } from '@addon/block/comments/comments.module'; import { AddonBlockCompletionStatusModule } from '@addon/block/completionstatus/completionstatus.module'; +import { AddonBlockGlossaryRandomModule } from '@addon/block/glossaryrandom/glossaryrandom.module'; import { AddonBlockHtmlModule } from '@addon/block/html/html.module'; import { AddonBlockMyOverviewModule } from '@addon/block/myoverview/myoverview.module'; import { AddonBlockNewsItemsModule } from '@addon/block/newsitems/newsitems.module'; @@ -227,6 +228,7 @@ export const CORE_PROVIDERS: any[] = [ AddonBlockCalendarUpcomingModule, AddonBlockCommentsModule, AddonBlockCompletionStatusModule, + AddonBlockGlossaryRandomModule, AddonBlockHtmlModule, AddonBlockLearningPlansModule, AddonBlockMyOverviewModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 88734646f4d..cde20c4dc95 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -31,6 +31,7 @@ "addon.block_calendarupcoming.pluginname": " Upcoming events", "addon.block_comments.pluginname": "Comments", "addon.block_completionstatus.pluginname": "Course completion status", + "addon.block_glossaryrandom.pluginname": "Random glossary entry", "addon.block_learningplans.pluginname": "Learning plans", "addon.block_myoverview.all": "All", "addon.block_myoverview.favourites": "Starred", From ebe4b0cf99452e665c99a7db10842c4fcde21d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 10 May 2019 15:34:31 +0200 Subject: [PATCH 031/241] MOBILE-3002 block: Add Latest badges block feature --- scripts/langindex.json | 1 + src/addon/block/badges/badges.module.ts | 38 ++++++++++++++ src/addon/block/badges/badges.scss | 23 ++++++++ src/addon/block/badges/lang/en.json | 3 ++ .../block/badges/providers/block-handler.ts | 52 +++++++++++++++++++ src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + 7 files changed, 120 insertions(+) create mode 100644 src/addon/block/badges/badges.module.ts create mode 100644 src/addon/block/badges/badges.scss create mode 100644 src/addon/block/badges/lang/en.json create mode 100644 src/addon/block/badges/providers/block-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 1c36c87eafa..b616afc3c42 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -27,6 +27,7 @@ "addon.badges.version": "badges", "addon.badges.warnexpired": "badges", "addon.block_activitymodules.pluginname": "block_activity_modules", + "addon.block_badges.pluginname": "block_badges", "addon.block_calendarmonth.pluginname": "block_calendar_month", "addon.block_calendarupcoming.pluginname": "block_calendar_upcoming", "addon.block_comments.pluginname": "block_comments", diff --git a/src/addon/block/badges/badges.module.ts b/src/addon/block/badges/badges.module.ts new file mode 100644 index 00000000000..ff9c450d293 --- /dev/null +++ b/src/addon/block/badges/badges.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockBadgesHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockBadgesHandler + ] +}) +export class AddonBlockBadgesModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockBadgesHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/badges/badges.scss b/src/addon/block/badges/badges.scss new file mode 100644 index 00000000000..3f9d26d8bac --- /dev/null +++ b/src/addon/block/badges/badges.scss @@ -0,0 +1,23 @@ +.addon-block-badges core-block-pre-rendered { + .core-block-content { + ul.badges { + list-style: none; + @include margin-horizontal(0); + -webkit-padding-start: 0; + + li { + position: relative; + display: inline-block; + padding-top: 1em; + text-align: center; + vertical-align: top; + width: 150px; + + .badge-name { + display: block; + padding: 5px; + } + } + } + } +} \ No newline at end of file diff --git a/src/addon/block/badges/lang/en.json b/src/addon/block/badges/lang/en.json new file mode 100644 index 00000000000..dd957321f57 --- /dev/null +++ b/src/addon/block/badges/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Latest badges" +} \ No newline at end of file diff --git a/src/addon/block/badges/providers/block-handler.ts b/src/addon/block/badges/providers/block-handler.ts new file mode 100644 index 00000000000..9cf9b36227e --- /dev/null +++ b/src/addon/block/badges/providers/block-handler.ts @@ -0,0 +1,52 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockBadgesHandler extends CoreBlockBaseHandler { + name = 'AddonBlockBadges'; + blockName = 'badges'; + + constructor(private translate: TranslateService) { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + + return { + title: this.translate.instant('addon.block_badges.pluginname'), + class: 'addon-block-badges', + component: CoreBlockPreRenderedComponent + }; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6069ee98333..1507b69210e 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -91,6 +91,7 @@ import { AddonCourseCompletionModule } from '@addon/coursecompletion/coursecompl import { AddonUserProfileFieldModule } from '@addon/userprofilefield/userprofilefield.module'; import { AddonFilesModule } from '@addon/files/files.module'; import { AddonBlockActivityModulesModule } from '@addon/block/activitymodules/activitymodules.module'; +import { AddonBlockBadgesModule } from '@addon/block/badges/badges.module'; import { AddonBlockCalendarMonthModule } from '@addon/block/calendarmonth/calendarmonth.module'; import { AddonBlockCalendarUpcomingModule } from '@addon/block/calendarupcoming/calendarupcoming.module'; import { AddonBlockCommentsModule } from '@addon/block/comments/comments.module'; @@ -224,6 +225,7 @@ export const CORE_PROVIDERS: any[] = [ AddonUserProfileFieldModule, AddonFilesModule, AddonBlockActivityModulesModule, + AddonBlockBadgesModule, AddonBlockCalendarMonthModule, AddonBlockCalendarUpcomingModule, AddonBlockCommentsModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index cde20c4dc95..fff9c6689e9 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -27,6 +27,7 @@ "addon.badges.version": "Version", "addon.badges.warnexpired": "(This badge has expired!)", "addon.block_activitymodules.pluginname": "Activities", + "addon.block_badges.pluginname": "Latest badges", "addon.block_calendarmonth.pluginname": "Calendar", "addon.block_calendarupcoming.pluginname": " Upcoming events", "addon.block_comments.pluginname": "Comments", From 48303896b927f1228e78ecc4392076a3b1619dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 10 May 2019 15:53:56 +0200 Subject: [PATCH 032/241] MOBILE-3002 block: Add Tags block feature --- scripts/langindex.json | 1 + src/addon/block/tags/lang/en.json | 3 + .../block/tags/providers/block-handler.ts | 52 +++++++++ src/addon/block/tags/tags.module.ts | 38 +++++++ src/addon/block/tags/tags.scss | 100 ++++++++++++++++++ src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + 7 files changed, 197 insertions(+) create mode 100644 src/addon/block/tags/lang/en.json create mode 100644 src/addon/block/tags/providers/block-handler.ts create mode 100644 src/addon/block/tags/tags.module.ts create mode 100644 src/addon/block/tags/tags.scss diff --git a/scripts/langindex.json b/scripts/langindex.json index b616afc3c42..ceb8559a8f1 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -56,6 +56,7 @@ "addon.block_sitemainmenu.pluginname": "block_site_main_menu", "addon.block_starredcourses.nocourses": "block_starredcourses", "addon.block_starredcourses.pluginname": "block_starredcourses", + "addon.block_tags.pluginname": "block_tags", "addon.block_timeline.duedate": "block_timeline", "addon.block_timeline.next30days": "block_timeline", "addon.block_timeline.next3months": "block_timeline", diff --git a/src/addon/block/tags/lang/en.json b/src/addon/block/tags/lang/en.json new file mode 100644 index 00000000000..a4080dd788f --- /dev/null +++ b/src/addon/block/tags/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Tags" +} \ No newline at end of file diff --git a/src/addon/block/tags/providers/block-handler.ts b/src/addon/block/tags/providers/block-handler.ts new file mode 100644 index 00000000000..749188709dd --- /dev/null +++ b/src/addon/block/tags/providers/block-handler.ts @@ -0,0 +1,52 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockTagsHandler extends CoreBlockBaseHandler { + name = 'AddonBlockTags'; + blockName = 'tags'; + + constructor(private translate: TranslateService) { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + + return { + title: this.translate.instant('addon.block_tags.pluginname'), + class: 'addon-block-tags', + component: CoreBlockPreRenderedComponent + }; + } +} diff --git a/src/addon/block/tags/tags.module.ts b/src/addon/block/tags/tags.module.ts new file mode 100644 index 00000000000..4b57911c0f0 --- /dev/null +++ b/src/addon/block/tags/tags.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockTagsHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockTagsHandler + ] +}) +export class AddonBlockTagsModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockTagsHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/tags/tags.scss b/src/addon/block/tags/tags.scss new file mode 100644 index 00000000000..cd3df32ebd0 --- /dev/null +++ b/src/addon/block/tags/tags.scss @@ -0,0 +1,100 @@ +.addon-block-tags core-block-pre-rendered { + .core-block-content { + .tag_cloud { + text-align: center; + ul.inline-list { + list-style: none; + @include margin-horizontal(0); + -webkit-padding-start: 0; + + li { + padding: 0 .2em; + display: inline; + } + } + } + .tag_cloud .s20 { + font-size: 2.7em; + } + + .tag_cloud .s19 { + font-size: 2.6em; + } + + .tag_cloud .s18 { + font-size: 2.5em; + } + + .tag_cloud .s17 { + font-size: 2.4em; + } + + .tag_cloud .s16 { + font-size: 2.3em; + } + + .tag_cloud .s15 { + font-size: 2.2em; + } + + .tag_cloud .s14 { + font-size: 2.1em; + } + + .tag_cloud .s13 { + font-size: 2em; + } + + .tag_cloud .s12 { + font-size: 1.9em; + } + + .tag_cloud .s11 { + font-size: 1.8em; + } + + .tag_cloud .s10 { + font-size: 1.7em; + } + + .tag_cloud .s9 { + font-size: 1.6em; + } + + .tag_cloud .s8 { + font-size: 1.5em; + } + + .tag_cloud .s7 { + font-size: 1.4em; + } + + .tag_cloud .s6 { + font-size: 1.3em; + } + + .tag_cloud .s5 { + font-size: 1.2em; + } + + .tag_cloud .s4 { + font-size: 1.1em; + } + + .tag_cloud .s3 { + font-size: 1em; + } + + .tag_cloud .s2 { + font-size: 0.9em; + } + + .tag_cloud .s1 { + font-size: 0.8em; + } + + .tag_cloud .s0 { + font-size: 0.7em; + } + } +} \ No newline at end of file diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1507b69210e..9b4d75af738 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -109,6 +109,7 @@ import { AddonBlockRecentlyAccessedCoursesModule } from '@addon/block/recentlyac import { AddonBlockRecentlyAccessedItemsModule } from '@addon/block/recentlyaccesseditems/recentlyaccesseditems.module'; import { AddonBlockStarredCoursesModule } from '@addon/block/starredcourses/starredcourses.module'; import { AddonBlockSelfCompletionModule } from '@addon/block/selfcompletion/selfcompletion.module'; +import { AddonBlockTagsModule } from '@addon/block/tags/tags.module'; import { AddonModAssignModule } from '@addon/mod/assign/assign.module'; import { AddonModBookModule } from '@addon/mod/book/book.module'; import { AddonModChatModule } from '@addon/mod/chat/chat.module'; @@ -243,6 +244,7 @@ export const CORE_PROVIDERS: any[] = [ AddonBlockRecentlyAccessedItemsModule, AddonBlockStarredCoursesModule, AddonBlockSelfCompletionModule, + AddonBlockTagsModule, AddonModAssignModule, AddonModBookModule, AddonModChatModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index fff9c6689e9..390eec79f4f 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -56,6 +56,7 @@ "addon.block_sitemainmenu.pluginname": "Main menu", "addon.block_starredcourses.nocourses": "No starred courses", "addon.block_starredcourses.pluginname": "Starred courses", + "addon.block_tags.pluginname": "Tags", "addon.block_timeline.duedate": "Due date", "addon.block_timeline.next30days": "Next 30 days", "addon.block_timeline.next3months": "Next 3 months", From 7398b0a662d25e5b542dcaeec4471b70f9e18ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 13 May 2019 13:30:02 +0200 Subject: [PATCH 033/241] MOBILE-3002 block: Add Blog Tags block feature --- scripts/langindex.json | 1 + src/addon/block/blogtags/blogtags.module.ts | 38 +++++++++ src/addon/block/blogtags/blogtags.scss | 82 +++++++++++++++++++ src/addon/block/blogtags/lang/en.json | 3 + .../block/blogtags/providers/block-handler.ts | 52 ++++++++++++ src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + 7 files changed, 179 insertions(+) create mode 100644 src/addon/block/blogtags/blogtags.module.ts create mode 100644 src/addon/block/blogtags/blogtags.scss create mode 100644 src/addon/block/blogtags/lang/en.json create mode 100644 src/addon/block/blogtags/providers/block-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index ceb8559a8f1..7ee470a7112 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -28,6 +28,7 @@ "addon.badges.warnexpired": "badges", "addon.block_activitymodules.pluginname": "block_activity_modules", "addon.block_badges.pluginname": "block_badges", + "addon.block_blogtags.pluginname": "block_blog_tags", "addon.block_calendarmonth.pluginname": "block_calendar_month", "addon.block_calendarupcoming.pluginname": "block_calendar_upcoming", "addon.block_comments.pluginname": "block_comments", diff --git a/src/addon/block/blogtags/blogtags.module.ts b/src/addon/block/blogtags/blogtags.module.ts new file mode 100644 index 00000000000..a9ed3a0904c --- /dev/null +++ b/src/addon/block/blogtags/blogtags.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockBlogTagsHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockBlogTagsHandler + ] +}) +export class AddonBlockBlogTagsModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockBlogTagsHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/blogtags/blogtags.scss b/src/addon/block/blogtags/blogtags.scss new file mode 100644 index 00000000000..859b876da81 --- /dev/null +++ b/src/addon/block/blogtags/blogtags.scss @@ -0,0 +1,82 @@ +.addon-block-blog-tags core-block-pre-rendered { + .core-block-content { + ul.inline-list { + list-style: none; + @include margin-horizontal(0); + -webkit-padding-start: 0; + + li { + padding: 0 .2em; + display: inline; + } + } + .s20 { + font-size: 1.5em; + font-weight: bold; + } + + .s19 { + font-size: 1.5em; + } + + .s18 { + font-size: 1.4em; + font-weight: bold; + } + + .s17 { + font-size: 1.4em; + } + + .s16 { + font-size: 1.3em; + font-weight: bold; + } + + .s15 { + font-size: 1.3em; + } + + .s14 { + font-size: 1.2em; + font-weight: bold; + } + + .s13 { + font-size: 1.2em; + } + + .s12, + .s11 { + font-size: 1.1em; + font-weight: bold; + } + + .s10, + .s9 { + font-size: 1.1em; + } + + .s8, + .s7 { + font-size: 1em; + font-weight: bold; + } + + .s6, + .s5 { + font-size: 1em; + } + + .s4, + .s3 { + font-size: 0.9em; + font-weight: bold; + } + + .s2, + .s1 { + font-size: 0.9em; + } + } +} \ No newline at end of file diff --git a/src/addon/block/blogtags/lang/en.json b/src/addon/block/blogtags/lang/en.json new file mode 100644 index 00000000000..683c3aa90af --- /dev/null +++ b/src/addon/block/blogtags/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Blog tags" +} \ No newline at end of file diff --git a/src/addon/block/blogtags/providers/block-handler.ts b/src/addon/block/blogtags/providers/block-handler.ts new file mode 100644 index 00000000000..cd6ae4c4677 --- /dev/null +++ b/src/addon/block/blogtags/providers/block-handler.ts @@ -0,0 +1,52 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockBlogTagsHandler extends CoreBlockBaseHandler { + name = 'AddonBlockBlogTags'; + blockName = 'blog_tags'; + + constructor(private translate: TranslateService) { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + + return { + title: this.translate.instant('addon.block_blogtags.pluginname'), + class: 'addon-block-blog-tags', + component: CoreBlockPreRenderedComponent + }; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 9b4d75af738..6183c41f358 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -92,6 +92,7 @@ import { AddonUserProfileFieldModule } from '@addon/userprofilefield/userprofile import { AddonFilesModule } from '@addon/files/files.module'; import { AddonBlockActivityModulesModule } from '@addon/block/activitymodules/activitymodules.module'; import { AddonBlockBadgesModule } from '@addon/block/badges/badges.module'; +import { AddonBlockBlogTagsModule } from '@addon/block/blogtags/blogtags.module'; import { AddonBlockCalendarMonthModule } from '@addon/block/calendarmonth/calendarmonth.module'; import { AddonBlockCalendarUpcomingModule } from '@addon/block/calendarupcoming/calendarupcoming.module'; import { AddonBlockCommentsModule } from '@addon/block/comments/comments.module'; @@ -227,6 +228,7 @@ export const CORE_PROVIDERS: any[] = [ AddonFilesModule, AddonBlockActivityModulesModule, AddonBlockBadgesModule, + AddonBlockBlogTagsModule, AddonBlockCalendarMonthModule, AddonBlockCalendarUpcomingModule, AddonBlockCommentsModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 390eec79f4f..8eba3906250 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -28,6 +28,7 @@ "addon.badges.warnexpired": "(This badge has expired!)", "addon.block_activitymodules.pluginname": "Activities", "addon.block_badges.pluginname": "Latest badges", + "addon.block_blogtags.pluginname": "Blog tags", "addon.block_calendarmonth.pluginname": "Calendar", "addon.block_calendarupcoming.pluginname": " Upcoming events", "addon.block_comments.pluginname": "Comments", From 83a899acd6bc66c34f9df0c68b5fc35504b51412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 13 May 2019 13:44:46 +0200 Subject: [PATCH 034/241] MOBILE-3002 block: Add Blog Menu block feature --- scripts/langindex.json | 1 + src/addon/block/blogmenu/blogmenu.module.ts | 38 ++++++++++++++ src/addon/block/blogmenu/blogmenu.scss | 16 ++++++ src/addon/block/blogmenu/lang/en.json | 3 ++ .../block/blogmenu/providers/block-handler.ts | 52 +++++++++++++++++++ src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + 7 files changed, 113 insertions(+) create mode 100644 src/addon/block/blogmenu/blogmenu.module.ts create mode 100644 src/addon/block/blogmenu/blogmenu.scss create mode 100644 src/addon/block/blogmenu/lang/en.json create mode 100644 src/addon/block/blogmenu/providers/block-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 7ee470a7112..b901aacbb35 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -28,6 +28,7 @@ "addon.badges.warnexpired": "badges", "addon.block_activitymodules.pluginname": "block_activity_modules", "addon.block_badges.pluginname": "block_badges", + "addon.block_blogmenu.pluginname": "block_blog_menu", "addon.block_blogtags.pluginname": "block_blog_tags", "addon.block_calendarmonth.pluginname": "block_calendar_month", "addon.block_calendarupcoming.pluginname": "block_calendar_upcoming", diff --git a/src/addon/block/blogmenu/blogmenu.module.ts b/src/addon/block/blogmenu/blogmenu.module.ts new file mode 100644 index 00000000000..cbca102001d --- /dev/null +++ b/src/addon/block/blogmenu/blogmenu.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockBlogMenuHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockBlogMenuHandler + ] +}) +export class AddonBlockBlogMenuModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockBlogMenuHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/blogmenu/blogmenu.scss b/src/addon/block/blogmenu/blogmenu.scss new file mode 100644 index 00000000000..c649986ed0e --- /dev/null +++ b/src/addon/block/blogmenu/blogmenu.scss @@ -0,0 +1,16 @@ +.addon-block-blog-menu core-block-pre-rendered { + .core-block-content { + ul.list { + list-style: none; + @include margin-horizontal(0); + -webkit-padding-start: 0; + + li { + padding-bottom: 8px; + } + } + } + .core-block-footer { + display: none; + } +} \ No newline at end of file diff --git a/src/addon/block/blogmenu/lang/en.json b/src/addon/block/blogmenu/lang/en.json new file mode 100644 index 00000000000..23541f7a0b0 --- /dev/null +++ b/src/addon/block/blogmenu/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Blog menu" +} \ No newline at end of file diff --git a/src/addon/block/blogmenu/providers/block-handler.ts b/src/addon/block/blogmenu/providers/block-handler.ts new file mode 100644 index 00000000000..362b978994a --- /dev/null +++ b/src/addon/block/blogmenu/providers/block-handler.ts @@ -0,0 +1,52 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockBlogMenuHandler extends CoreBlockBaseHandler { + name = 'AddonBlockBlogMenu'; + blockName = 'blog_menu'; + + constructor(private translate: TranslateService) { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + + return { + title: this.translate.instant('addon.block_blogmenu.pluginname'), + class: 'addon-block-blog-menu', + component: CoreBlockPreRenderedComponent + }; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6183c41f358..10a4b4b1aff 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -92,6 +92,7 @@ import { AddonUserProfileFieldModule } from '@addon/userprofilefield/userprofile import { AddonFilesModule } from '@addon/files/files.module'; import { AddonBlockActivityModulesModule } from '@addon/block/activitymodules/activitymodules.module'; import { AddonBlockBadgesModule } from '@addon/block/badges/badges.module'; +import { AddonBlockBlogMenuModule } from '@addon/block/blogmenu/blogmenu.module'; import { AddonBlockBlogTagsModule } from '@addon/block/blogtags/blogtags.module'; import { AddonBlockCalendarMonthModule } from '@addon/block/calendarmonth/calendarmonth.module'; import { AddonBlockCalendarUpcomingModule } from '@addon/block/calendarupcoming/calendarupcoming.module'; @@ -228,6 +229,7 @@ export const CORE_PROVIDERS: any[] = [ AddonFilesModule, AddonBlockActivityModulesModule, AddonBlockBadgesModule, + AddonBlockBlogMenuModule, AddonBlockBlogTagsModule, AddonBlockCalendarMonthModule, AddonBlockCalendarUpcomingModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 8eba3906250..b96c9a9706a 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -28,6 +28,7 @@ "addon.badges.warnexpired": "(This badge has expired!)", "addon.block_activitymodules.pluginname": "Activities", "addon.block_badges.pluginname": "Latest badges", + "addon.block_blogmenu.pluginname": "Blog menu", "addon.block_blogtags.pluginname": "Blog tags", "addon.block_calendarmonth.pluginname": "Calendar", "addon.block_calendarupcoming.pluginname": " Upcoming events", From 8ede493cef203b80b70ab5b037cd98a21f22ddc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 14 May 2019 13:51:26 +0200 Subject: [PATCH 035/241] MOBILE-3002 block: Add Recent activity block feature --- scripts/langindex.json | 3 +- src/addon/block/recentactivity/lang/en.json | 3 ++ .../recentactivity/providers/block-handler.ts | 52 +++++++++++++++++++ .../recentactivity/recentactivity.module.ts | 38 ++++++++++++++ .../block/recentactivity/recentactivity.scss | 20 +++++++ src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + 7 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 src/addon/block/recentactivity/lang/en.json create mode 100644 src/addon/block/recentactivity/providers/block-handler.ts create mode 100644 src/addon/block/recentactivity/recentactivity.module.ts create mode 100644 src/addon/block/recentactivity/recentactivity.scss diff --git a/scripts/langindex.json b/scripts/langindex.json index b901aacbb35..b803d75bfb0 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -53,7 +53,8 @@ "addon.block_recentlyaccessedcourses.nocourses": "block_recentlyaccessedcourses", "addon.block_recentlyaccessedcourses.pluginname": "block_recentlyaccessedcourses", "addon.block_recentlyaccesseditems.noitems": "block_recentlyaccesseditems", - "addon.block_recentlyaccesseditems.pluginname": "block_recentlyaccesseditems", + "addon.block_recentactivity.pluginname": "block_recent_activity", + "addon.block_glossaryrandom.pluginname": "block_glossary_random", "addon.block_selfcompletion.pluginname": "block_selfcompletion", "addon.block_sitemainmenu.pluginname": "block_site_main_menu", "addon.block_starredcourses.nocourses": "block_starredcourses", diff --git a/src/addon/block/recentactivity/lang/en.json b/src/addon/block/recentactivity/lang/en.json new file mode 100644 index 00000000000..29f7996e2a6 --- /dev/null +++ b/src/addon/block/recentactivity/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Recent activity" +} \ No newline at end of file diff --git a/src/addon/block/recentactivity/providers/block-handler.ts b/src/addon/block/recentactivity/providers/block-handler.ts new file mode 100644 index 00000000000..043acd495a6 --- /dev/null +++ b/src/addon/block/recentactivity/providers/block-handler.ts @@ -0,0 +1,52 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockRecentActivityHandler extends CoreBlockBaseHandler { + name = 'AddonBlockRecentActivity'; + blockName = 'recent_activity'; + + constructor(private translate: TranslateService) { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + + return { + title: this.translate.instant('addon.block_recentactivity.pluginname'), + class: 'addon-block-recent-activity', + component: CoreBlockPreRenderedComponent + }; + } +} diff --git a/src/addon/block/recentactivity/recentactivity.module.ts b/src/addon/block/recentactivity/recentactivity.module.ts new file mode 100644 index 00000000000..cd0136763ef --- /dev/null +++ b/src/addon/block/recentactivity/recentactivity.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockRecentActivityHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockRecentActivityHandler + ] +}) +export class AddonBlockRecentActivityModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockRecentActivityHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/recentactivity/recentactivity.scss b/src/addon/block/recentactivity/recentactivity.scss new file mode 100644 index 00000000000..0eb73e10255 --- /dev/null +++ b/src/addon/block/recentactivity/recentactivity.scss @@ -0,0 +1,20 @@ +.addon-block-recent-activity core-block-pre-rendered { + .core-block-content { + .activitydate, .activityhead { + text-align: center; + } + + .unlist { + list-style: none; + @include margin-horizontal(0); + -webkit-padding-start: 0; + li { + margin-bottom: 1em; + + .head .date { + @include float(end); + } + } + } + } +} \ No newline at end of file diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 10a4b4b1aff..cc570b6b2ae 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -109,6 +109,7 @@ import { AddonBlockSiteMainMenuModule } from '@addon/block/sitemainmenu/sitemain import { AddonBlockTimelineModule } from '@addon/block/timeline/timeline.module'; import { AddonBlockRecentlyAccessedCoursesModule } from '@addon/block/recentlyaccessedcourses/recentlyaccessedcourses.module'; import { AddonBlockRecentlyAccessedItemsModule } from '@addon/block/recentlyaccesseditems/recentlyaccesseditems.module'; +import { AddonBlockRecentActivityModule } from '@addon/block/recentactivity/recentactivity.module'; import { AddonBlockStarredCoursesModule } from '@addon/block/starredcourses/starredcourses.module'; import { AddonBlockSelfCompletionModule } from '@addon/block/selfcompletion/selfcompletion.module'; import { AddonBlockTagsModule } from '@addon/block/tags/tags.module'; @@ -246,6 +247,7 @@ export const CORE_PROVIDERS: any[] = [ AddonBlockTimelineModule, AddonBlockRecentlyAccessedCoursesModule, AddonBlockRecentlyAccessedItemsModule, + AddonBlockRecentActivityModule, AddonBlockStarredCoursesModule, AddonBlockSelfCompletionModule, AddonBlockTagsModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index b96c9a9706a..69e66a2b36f 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -50,6 +50,7 @@ "addon.block_newsitems.pluginname": "Latest announcements", "addon.block_onlineusers.pluginname": "Online users", "addon.block_privatefiles.pluginname": "Private files", + "addon.block_recentactivity.pluginname": "Recent activity", "addon.block_recentlyaccessedcourses.nocourses": "No recent courses", "addon.block_recentlyaccessedcourses.pluginname": "Recently accessed courses", "addon.block_recentlyaccesseditems.noitems": "No recent items", From cac7f0bc1255ca0cdd183ba70f4206f72e70b56a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 14 May 2019 14:01:08 +0200 Subject: [PATCH 036/241] MOBILE-3002 block: Add Recent blog entries block feature --- scripts/langindex.json | 1 + .../block/blogrecent/blogrecent.module.ts | 38 ++++++++++++++ src/addon/block/blogrecent/blogrecent.scss | 13 +++++ src/addon/block/blogrecent/lang/en.json | 3 ++ .../blogrecent/providers/block-handler.ts | 52 +++++++++++++++++++ src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + 7 files changed, 110 insertions(+) create mode 100644 src/addon/block/blogrecent/blogrecent.module.ts create mode 100644 src/addon/block/blogrecent/blogrecent.scss create mode 100644 src/addon/block/blogrecent/lang/en.json create mode 100644 src/addon/block/blogrecent/providers/block-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index b803d75bfb0..95f5d1ca6a7 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -29,6 +29,7 @@ "addon.block_activitymodules.pluginname": "block_activity_modules", "addon.block_badges.pluginname": "block_badges", "addon.block_blogmenu.pluginname": "block_blog_menu", + "addon.block_blogrecent.nocourses": "block_blog_recent", "addon.block_blogtags.pluginname": "block_blog_tags", "addon.block_calendarmonth.pluginname": "block_calendar_month", "addon.block_calendarupcoming.pluginname": "block_calendar_upcoming", diff --git a/src/addon/block/blogrecent/blogrecent.module.ts b/src/addon/block/blogrecent/blogrecent.module.ts new file mode 100644 index 00000000000..1ecdf6f7b36 --- /dev/null +++ b/src/addon/block/blogrecent/blogrecent.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockBlogRecentHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockBlogRecentHandler + ] +}) +export class AddonBlockBlogRecentModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockBlogRecentHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/blogrecent/blogrecent.scss b/src/addon/block/blogrecent/blogrecent.scss new file mode 100644 index 00000000000..81a03b22766 --- /dev/null +++ b/src/addon/block/blogrecent/blogrecent.scss @@ -0,0 +1,13 @@ +.addon-block-blog-recent core-block-pre-rendered { + .core-block-content { + ul.list { + list-style: none; + @include margin-horizontal(0); + -webkit-padding-start: 0; + + li { + padding-bottom: 8px; + } + } + } +} \ No newline at end of file diff --git a/src/addon/block/blogrecent/lang/en.json b/src/addon/block/blogrecent/lang/en.json new file mode 100644 index 00000000000..a92c0cce56e --- /dev/null +++ b/src/addon/block/blogrecent/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Recent blog entries" +} \ No newline at end of file diff --git a/src/addon/block/blogrecent/providers/block-handler.ts b/src/addon/block/blogrecent/providers/block-handler.ts new file mode 100644 index 00000000000..69140e96dc1 --- /dev/null +++ b/src/addon/block/blogrecent/providers/block-handler.ts @@ -0,0 +1,52 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockBlogRecentHandler extends CoreBlockBaseHandler { + name = 'AddonBlockBlogRecent'; + blockName = 'blog_recent'; + + constructor(private translate: TranslateService) { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + + return { + title: this.translate.instant('addon.block_blogrecent.pluginname'), + class: 'addon-block-blog-recent', + component: CoreBlockPreRenderedComponent + }; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index cc570b6b2ae..4c1b3f81f65 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -94,6 +94,7 @@ import { AddonBlockActivityModulesModule } from '@addon/block/activitymodules/ac import { AddonBlockBadgesModule } from '@addon/block/badges/badges.module'; import { AddonBlockBlogMenuModule } from '@addon/block/blogmenu/blogmenu.module'; import { AddonBlockBlogTagsModule } from '@addon/block/blogtags/blogtags.module'; +import { AddonBlockBlogRecentModule } from '@addon/block/blogrecent/blogrecent.module'; import { AddonBlockCalendarMonthModule } from '@addon/block/calendarmonth/calendarmonth.module'; import { AddonBlockCalendarUpcomingModule } from '@addon/block/calendarupcoming/calendarupcoming.module'; import { AddonBlockCommentsModule } from '@addon/block/comments/comments.module'; @@ -231,6 +232,7 @@ export const CORE_PROVIDERS: any[] = [ AddonBlockActivityModulesModule, AddonBlockBadgesModule, AddonBlockBlogMenuModule, + AddonBlockBlogRecentModule, AddonBlockBlogTagsModule, AddonBlockCalendarMonthModule, AddonBlockCalendarUpcomingModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 69e66a2b36f..a70d495545a 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -29,6 +29,7 @@ "addon.block_activitymodules.pluginname": "Activities", "addon.block_badges.pluginname": "Latest badges", "addon.block_blogmenu.pluginname": "Blog menu", + "addon.block_blogrecent.pluginname": "Recent blog entries", "addon.block_blogtags.pluginname": "Blog tags", "addon.block_calendarmonth.pluginname": "Calendar", "addon.block_calendarupcoming.pluginname": " Upcoming events", From a549e70d5f49b5cb3baa7aa19987c7718c7ea6ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 14 May 2019 14:44:33 +0200 Subject: [PATCH 037/241] MOBILE-3002 block: Add Remote RSS block feature --- scripts/langindex.json | 1 + src/addon/block/rssclient/lang/en.json | 3 ++ .../rssclient/providers/block-handler.ts | 52 +++++++++++++++++++ src/addon/block/rssclient/rssclient.module.ts | 38 ++++++++++++++ src/addon/block/rssclient/rssclient.scss | 19 +++++++ src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + 7 files changed, 116 insertions(+) create mode 100644 src/addon/block/rssclient/lang/en.json create mode 100644 src/addon/block/rssclient/providers/block-handler.ts create mode 100644 src/addon/block/rssclient/rssclient.module.ts create mode 100644 src/addon/block/rssclient/rssclient.scss diff --git a/scripts/langindex.json b/scripts/langindex.json index 95f5d1ca6a7..e29d5b9d70a 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -55,6 +55,7 @@ "addon.block_recentlyaccessedcourses.pluginname": "block_recentlyaccessedcourses", "addon.block_recentlyaccesseditems.noitems": "block_recentlyaccesseditems", "addon.block_recentactivity.pluginname": "block_recent_activity", + "addon.block_rssclient.pluginname": "block_rss_client", "addon.block_glossaryrandom.pluginname": "block_glossary_random", "addon.block_selfcompletion.pluginname": "block_selfcompletion", "addon.block_sitemainmenu.pluginname": "block_site_main_menu", diff --git a/src/addon/block/rssclient/lang/en.json b/src/addon/block/rssclient/lang/en.json new file mode 100644 index 00000000000..18282971b14 --- /dev/null +++ b/src/addon/block/rssclient/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Remote RSS feeds" +} \ No newline at end of file diff --git a/src/addon/block/rssclient/providers/block-handler.ts b/src/addon/block/rssclient/providers/block-handler.ts new file mode 100644 index 00000000000..ce26caba41e --- /dev/null +++ b/src/addon/block/rssclient/providers/block-handler.ts @@ -0,0 +1,52 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockRssClientHandler extends CoreBlockBaseHandler { + name = 'AddonBlockRssClient'; + blockName = 'rss_client'; + + constructor(private translate: TranslateService) { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param {Injector} injector Injector. + * @param {any} block The block to render. + * @param {string} contextLevel The context where the block will be used. + * @param {number} instanceId The instance ID associated with the context level. + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + + return { + title: block.contents.title || this.translate.instant('addon.block_rssclient.pluginname'), + class: 'addon-block-rss-client', + component: CoreBlockPreRenderedComponent + }; + } +} diff --git a/src/addon/block/rssclient/rssclient.module.ts b/src/addon/block/rssclient/rssclient.module.ts new file mode 100644 index 00000000000..b98c39ae544 --- /dev/null +++ b/src/addon/block/rssclient/rssclient.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockRssClientHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockRssClientHandler + ] +}) +export class AddonBlockRssClientModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockRssClientHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/rssclient/rssclient.scss b/src/addon/block/rssclient/rssclient.scss new file mode 100644 index 00000000000..33b37b0e75d --- /dev/null +++ b/src/addon/block/rssclient/rssclient.scss @@ -0,0 +1,19 @@ +.addon-block-rss-client core-block-pre-rendered { + .core-block-content { + .list { + list-style: none; + @include margin-horizontal(0); + -webkit-padding-start: 0; + + li { + border-top: 1px solid $gray; + padding: 5px; + padding-bottom: 8px; + } + + li:first-child { + border-top-width: 0; + } + } + } +} \ No newline at end of file diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 4c1b3f81f65..6969e01bc1b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -111,6 +111,7 @@ import { AddonBlockTimelineModule } from '@addon/block/timeline/timeline.module' import { AddonBlockRecentlyAccessedCoursesModule } from '@addon/block/recentlyaccessedcourses/recentlyaccessedcourses.module'; import { AddonBlockRecentlyAccessedItemsModule } from '@addon/block/recentlyaccesseditems/recentlyaccesseditems.module'; import { AddonBlockRecentActivityModule } from '@addon/block/recentactivity/recentactivity.module'; +import { AddonBlockRssClientModule } from '@addon/block/rssclient/rssclient.module'; import { AddonBlockStarredCoursesModule } from '@addon/block/starredcourses/starredcourses.module'; import { AddonBlockSelfCompletionModule } from '@addon/block/selfcompletion/selfcompletion.module'; import { AddonBlockTagsModule } from '@addon/block/tags/tags.module'; @@ -250,6 +251,7 @@ export const CORE_PROVIDERS: any[] = [ AddonBlockRecentlyAccessedCoursesModule, AddonBlockRecentlyAccessedItemsModule, AddonBlockRecentActivityModule, + AddonBlockRssClientModule, AddonBlockStarredCoursesModule, AddonBlockSelfCompletionModule, AddonBlockTagsModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index a70d495545a..f3f750318e4 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -56,6 +56,7 @@ "addon.block_recentlyaccessedcourses.pluginname": "Recently accessed courses", "addon.block_recentlyaccesseditems.noitems": "No recent items", "addon.block_recentlyaccesseditems.pluginname": "Recently accessed items", + "addon.block_rssclient.pluginname": "Remote RSS feeds", "addon.block_selfcompletion.pluginname": "Self completion", "addon.block_sitemainmenu.pluginname": "Main menu", "addon.block_starredcourses.nocourses": "No starred courses", From 03b305662e3c479127c4180847e277f0b7922a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 16 May 2019 11:16:24 +0200 Subject: [PATCH 038/241] MOBILE-3002 block: Check if blocks are disabled in courses --- src/core/block/providers/course-option-handler.ts | 5 +++-- src/core/block/providers/delegate.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/core/block/providers/course-option-handler.ts b/src/core/block/providers/course-option-handler.ts index 7e38227f524..893904ed81d 100644 --- a/src/core/block/providers/course-option-handler.ts +++ b/src/core/block/providers/course-option-handler.ts @@ -16,6 +16,7 @@ import { Injectable, Injector } from '@angular/core'; import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '@core/course/providers/options-delegate'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreBlockCourseBlocksComponent } from '../components/course-blocks/course-blocks'; +import { CoreBlockDelegate } from './delegate'; /** * Course nav handler. @@ -25,7 +26,7 @@ export class CoreBlockCourseBlocksCourseOptionHandler implements CoreCourseOptio name = 'CoreCourseBlocks'; priority = 700; - constructor(private courseProvider: CoreCourseProvider) {} + constructor(private courseProvider: CoreCourseProvider, private blockDelegate: CoreBlockDelegate) {} /** * Should invalidate the data to determine if the handler is enabled for a certain course. @@ -45,7 +46,7 @@ export class CoreBlockCourseBlocksCourseOptionHandler implements CoreCourseOptio * @return {boolean} Whether or not the handler is enabled on a site level. */ isEnabled(): boolean | Promise { - return this.courseProvider.canGetCourseBlocks(); + return this.courseProvider.canGetCourseBlocks() && !this.blockDelegate.areBlocksDisabledInCourses(); } /** diff --git a/src/core/block/providers/delegate.ts b/src/core/block/providers/delegate.ts index b4e6fcf4038..7f0f245f363 100644 --- a/src/core/block/providers/delegate.ts +++ b/src/core/block/providers/delegate.ts @@ -117,6 +117,18 @@ export class CoreBlockDelegate extends CoreDelegate { return site.isFeatureDisabled('NoDelegate_SiteBlocks'); } + /** + * Check if blocks are disabled in a certain site for courses. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether it's disabled. + */ + areBlocksDisabledInCourses(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.isFeatureDisabled('NoDelegate_CourseBlocks'); + } + /** * Check if blocks are disabled in a certain site. * From f597abd33f5420618b45b0bc4b7f5bc36e142864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 26 Jun 2019 16:41:56 +0200 Subject: [PATCH 039/241] MOBILE-1332 notes: Delete notes in offline mode --- .../components/list/addon-notes-list.html | 15 ++- src/addon/notes/components/list/list.ts | 46 +++++--- src/addon/notes/providers/notes-offline.ts | 86 +++++++++++++- src/addon/notes/providers/notes-sync.ts | 108 +++++++++++++----- src/addon/notes/providers/notes.ts | 77 +++++++++++-- 5 files changed, 275 insertions(+), 57 deletions(-) diff --git a/src/addon/notes/components/list/addon-notes-list.html b/src/addon/notes/components/list/addon-notes-list.html index fbbb27c7898..7908279f811 100644 --- a/src/addon/notes/components/list/addon-notes-list.html +++ b/src/addon/notes/components/list/addon-notes-list.html @@ -38,9 +38,18 @@

{{user.fullname}}

{{note.userfullname}}

-

{{note.lastmodified | coreDateDayOrTime}}

-

{{ 'core.notsent' | translate }}

- +
diff --git a/src/addon/notes/components/list/list.ts b/src/addon/notes/components/list/list.ts index 78f439efd01..e9b6ffd7a17 100644 --- a/src/addon/notes/components/list/list.ts +++ b/src/addon/notes/components/list/list.ts @@ -22,6 +22,7 @@ import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUserProvider } from '@core/user/providers/user'; import { coreSlideInOut } from '@classes/animations'; import { AddonNotesProvider } from '../../providers/notes'; +import { AddonNotesOfflineProvider } from '../../providers/notes-offline'; import { AddonNotesSyncProvider } from '../../providers/notes-sync'; /** @@ -54,7 +55,8 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, private modalCtrl: ModalController, private notesProvider: AddonNotesProvider, private notesSync: AddonNotesSyncProvider, - private userProvider: CoreUserProvider, private translate: TranslateService) { + private userProvider: CoreUserProvider, private translate: TranslateService, + private notesOffline: AddonNotesOfflineProvider) { // Refresh data if notes are synchronized automatically. this.syncObserver = eventsProvider.on(AddonNotesSyncProvider.AUTO_SYNCED, (data) => { if (data.courseId == this.courseId) { @@ -101,20 +103,23 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { return this.notesProvider.getNotes(this.courseId, this.userId).then((notes) => { notes = notes[this.type + 'notes'] || []; - this.hasOffline = notes.some((note) => note.offline); + return this.notesProvider.setOfflineDeletedNotes(notes, this.courseId).then((notes) => { - if (this.userId) { - this.notes = notes; + this.hasOffline = notes.some((note) => note.offline || note.deleted); - // Get the user profile to retrieve the user image. - return this.userProvider.getProfile(this.userId, this.courseId, true).then((user) => { - this.user = user; - }); - } else { - return this.notesProvider.getNotesUserData(notes, this.courseId).then((notes) => { + if (this.userId) { this.notes = notes; - }); - } + + // Get the user profile to retrieve the user image. + return this.userProvider.getProfile(this.userId, this.courseId, true).then((user) => { + this.user = user; + }); + } else { + return this.notesProvider.getNotesUserData(notes, this.courseId).then((notes) => { + this.notes = notes; + }); + } + }); }); }).catch((message) => { this.domUtils.showErrorModal(message); @@ -201,7 +206,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { e.stopPropagation(); this.domUtils.showConfirm(this.translate.instant('addon.notes.deleteconfirm')).then(() => { - this.notesProvider.deleteNote(note).then(() => { + this.notesProvider.deleteNote(note, this.courseId).then(() => { this.showDelete = false; this.refreshNotes(true); @@ -215,6 +220,21 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { }); } + /** + * Restore a note. + * + * @param {Event} e Click event. + * @param {any} note Note to delete. + */ + undoDeleteNote(e: Event, note: any): void { + e.preventDefault(); + e.stopPropagation(); + + this.notesOffline.undoDeleteNote(note.id).then(() => { + this.refreshNotes(true); + }); + } + /** * Toggle delete. */ diff --git a/src/addon/notes/providers/notes-offline.ts b/src/addon/notes/providers/notes-offline.ts index 486fa0111d1..28bae97bfb5 100644 --- a/src/addon/notes/providers/notes-offline.ts +++ b/src/addon/notes/providers/notes-offline.ts @@ -26,9 +26,10 @@ export class AddonNotesOfflineProvider { // Variables for database. static NOTES_TABLE = 'addon_notes_offline_notes'; + static NOTES_DELETED_TABLE = 'addon_notes_deleted_offline_notes'; protected siteSchema: CoreSiteSchema = { name: 'AddonNotesOfflineProvider', - version: 1, + version: 2, tables: [ { name: AddonNotesOfflineProvider.NOTES_TABLE, @@ -63,6 +64,24 @@ export class AddonNotesOfflineProvider { } ], primaryKeys: ['userid', 'content', 'created'] + }, + { + name: AddonNotesOfflineProvider.NOTES_DELETED_TABLE, + columns: [ + { + name: 'noteid', + type: 'INTEGER', + primaryKey: true + }, + { + name: 'deleted', + type: 'INTEGER' + }, + { + name: 'courseid', + type: 'INTEGER' + } + ] } ] }; @@ -73,7 +92,7 @@ export class AddonNotesOfflineProvider { } /** - * Delete a note. + * Delete an offline note. * * @param {number} userId User ID the note is about. * @param {string} content The note content. @@ -81,7 +100,7 @@ export class AddonNotesOfflineProvider { * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved if deleted, rejected if failure. */ - deleteNote(userId: number, content: string, timecreated: number, siteId?: string): Promise { + deleteOfflineNote(userId: number, content: string, timecreated: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.getDb().deleteRecords(AddonNotesOfflineProvider.NOTES_TABLE, { userid: userId, @@ -91,6 +110,31 @@ export class AddonNotesOfflineProvider { }); } + /** + * Get all offline deleted notes. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with notes. + */ + getAllDeletedNotes(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(AddonNotesOfflineProvider.NOTES_DELETED_TABLE); + }); + } + + /** + * Get course offline deleted notes. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with notes. + */ + getCourseDeletedNotes(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(AddonNotesOfflineProvider.NOTES_DELETED_TABLE, {courseid: courseId}); + }); + } + /** * Get all offline notes. * @@ -246,4 +290,40 @@ export class AddonNotesOfflineProvider { }); }); } + + /** + * Delete a note offline to be sent later. + * + * @param {number} noteId Note ID. + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + deleteNote(noteId: number, courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const now = this.timeUtils.timestamp(); + const data = { + noteid: noteId, + courseid: courseId, + deleted: now + }; + + return site.getDb().insertRecord(AddonNotesOfflineProvider.NOTES_DELETED_TABLE, data).then(() => { + return data; + }); + }); + } + + /** + * Undo delete a note. + * + * @param {number} noteId Note ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + undoDeleteNote(noteId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().deleteRecords(AddonNotesOfflineProvider.NOTES_DELETED_TABLE, { noteid: noteId }); + }); + } } diff --git a/src/addon/notes/providers/notes-sync.ts b/src/addon/notes/providers/notes-sync.ts index 77b7039c3e5..cebf92d73d8 100644 --- a/src/addon/notes/providers/notes-sync.ts +++ b/src/addon/notes/providers/notes-sync.ts @@ -63,18 +63,24 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider { * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. */ private syncAllNotesFunc(siteId: string, force: boolean): Promise { - return this.notesOffline.getAllNotes(siteId).then((notes) => { + const proms = []; + + proms.push(this.notesOffline.getAllNotes(siteId)); + proms.push(this.notesOffline.getAllDeletedNotes(siteId)); + + return Promise.all(proms).then((notesArray) => { // Get all the courses to be synced. - const courseIds = []; - notes.forEach((note) => { - if (courseIds.indexOf(note.courseid) == -1) { - courseIds.push(note.courseid); - } + const courseIds = {}; + notesArray.forEach((notes) => { + notes.forEach((note) => { + courseIds[note.courseid] = note.courseid; + }); }); - // Sync all courses. - const promises = courseIds.map((courseId) => { - const promise = force ? this.syncNotes(courseId, siteId) : this.syncNotesIfNeeded(courseId, siteId); + const promises = Object.keys(courseIds).map((courseId) => { + const cId = parseInt(courseIds[courseId], 10); + + const promise = force ? this.syncNotes(cId, siteId) : this.syncNotesIfNeeded(cId, siteId); return promise.then((warnings) => { if (typeof warnings != 'undefined') { @@ -124,9 +130,12 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider { this.logger.debug('Try to sync notes for course ' + courseId); const warnings = []; + const errors = []; + + const proms = []; // Get offline notes to be sent. - const syncPromise = this.notesOffline.getNotesForCourse(courseId, siteId).then((notes) => { + proms.push(this.notesOffline.getNotesForCourse(courseId, siteId).then((notes) => { if (!notes.length) { // Nothing to sync. return; @@ -157,12 +166,6 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider { } }); - // Fetch the notes from server to be sure they're up to date. - return this.notesProvider.invalidateNotes(courseId, undefined, siteId).then(() => { - return this.notesProvider.getNotes(courseId, undefined, false, true, siteId); - }).catch(() => { - // Ignore errors. - }); }).catch((error) => { if (this.utils.isWebServiceError(error)) { // It's a WebService error, this means the user cannot send notes. @@ -174,26 +177,69 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider { }).then(() => { // Notes were sent, delete them from local DB. const promises = notes.map((note) => { - return this.notesOffline.deleteNote(note.userid, note.content, note.created, siteId); + return this.notesOffline.deleteOfflineNote(note.userid, note.content, note.created, siteId); }); return Promise.all(promises); - }).then(() => { - if (errors && errors.length) { - // At least an error occurred, get course name and add errors to warnings array. - return this.coursesProvider.getUserCourse(courseId, true, siteId).catch(() => { - // Ignore errors. - return {}; - }).then((course) => { - errors.forEach((error) => { - warnings.push(this.translate.instant('addon.notes.warningnotenotsent', { - course: course.fullname ? course.fullname : courseId, - error: error - })); - }); - }); + }); + })); + + // Get offline notes to be sent. + proms.push(this.notesOffline.getCourseDeletedNotes(courseId, siteId).then((notes) => { + if (!notes.length) { + // Nothing to sync. + return; + } else if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + return Promise.reject(this.translate.instant('core.networkerrormsg')); + } + + // Format the notes to be sent. + const notesToDelete = notes.map((note) => { + return note.noteid; + }); + + // Delete the notes. + return this.notesProvider.deleteNotesOnline(notesToDelete, courseId, siteId).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, this means the user cannot send notes. + errors.push(error); + } else { + // Not a WebService error, reject the synchronization to try again. + return Promise.reject(error); } + }).then(() => { + // Notes were sent, delete them from local DB. + const promises = notes.map((noteId) => { + return this.notesOffline.undoDeleteNote(noteId, siteId); + }); + + return Promise.all(promises); + }); + })); + + const syncPromise = Promise.all(proms).then(() => { + // Fetch the notes from server to be sure they're up to date. + return this.notesProvider.invalidateNotes(courseId, undefined, siteId).then(() => { + return this.notesProvider.getNotes(courseId, undefined, false, true, siteId); + }).catch(() => { + // Ignore errors. }); + }).then(() => { + if (errors && errors.length) { + // At least an error occurred, get course name and add errors to warnings array. + return this.coursesProvider.getUserCourse(courseId, true, siteId).catch(() => { + // Ignore errors. + return {}; + }).then((course) => { + errors.forEach((error) => { + warnings.push(this.translate.instant('addon.notes.warningnotenotsent', { + course: course.fullname ? course.fullname : courseId, + error: error + })); + }); + }); + } }).then(() => { // All done, return the warnings. return warnings; diff --git a/src/addon/notes/providers/notes.ts b/src/addon/notes/providers/notes.ts index 52caa2c37b7..82f095e4173 100644 --- a/src/addon/notes/providers/notes.ts +++ b/src/addon/notes/providers/notes.ts @@ -137,20 +137,65 @@ export class AddonNotesProvider { * Delete a note. * * @param {any} note Note object to delete. + * @param {number} courseId Course ID where the note belongs. * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved when done. + * @return {Promise} Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that notes + * have been deleted, the resolve param can contain errors for notes not deleted. */ - deleteNote(note: any, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - if (typeof note.offline != 'undefined' && note.offline) { - return this.notesOffline.deleteNote(note.userid, note.content, note.created, site.id); + deleteNote(note: any, courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (note.offline) { + return this.notesOffline.deleteOfflineNote(note.userid, note.content, note.created, siteId); + } + + // Convenience function to store the action to be synchronized later. + const storeOffline = (): Promise => { + return this.notesOffline.deleteNote(note.id, courseId, siteId).then(() => { + return false; + }); + }; + + if (!this.appProvider.isOnline()) { + // App is offline, store the note. + return storeOffline(); + } + + // Send note to server. + return this.deleteNotesOnline([note.id], courseId, siteId).then(() => { + return true; + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, the user cannot send the note so don't store it. + return Promise.reject(error); } + // Error sending note, store it to retry later. + return storeOffline(); + }); + } + + /** + * Delete a note. It will fail if offline or cannot connect. + * + * @param {number[]} noteIds Note IDs to delete. + * @param {number} courseId Course ID where the note belongs. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that notes + * have been deleted, the resolve param can contain errors for notes not deleted. + */ + deleteNotesOnline(noteIds: number[], courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const data = { - notes: [note.id] + notes: noteIds }; - return site.write('core_notes_delete_notes', data); + return site.write('core_notes_delete_notes', data).then((response) => { + // A note was deleted, invalidate the course notes. + return this.invalidateNotes(courseId, undefined, siteId).catch(() => { + // Ignore errors. + }); + }); }); } @@ -288,6 +333,24 @@ export class AddonNotesProvider { }); } + /** + * Get offline deleted notes and set the state. + * + * @param {any[]} notes Array of notes. + * @param {number} courseId ID of the course the notes belong to. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} [description] + */ + setOfflineDeletedNotes(notes: any[], courseId: number, siteId?: string): Promise { + return this.notesOffline.getCourseDeletedNotes(courseId, siteId).then((deletedNotes) => { + notes.forEach((note) => { + note.deleted = deletedNotes.some((n) => n.noteid == note.id); + }); + + return notes; + }); + } + /** * Get user data for notes since they only have userid. * From 041206993f359bf8d3f9a7a52eb9042cf17780b3 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 8 Jul 2019 10:42:56 +0200 Subject: [PATCH 040/241] MOBILE-2808 core: Display month name in ion-datetime --- src/addon/calendar/pages/event/event.ts | 2 +- .../mod/data/fields/date/component/date.ts | 2 +- .../datetime/component/datetime.ts | 2 +- src/providers/lang.ts | 22 +++++++++++++++++-- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index 568eac629a8..0f7fed557ee 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -73,7 +73,7 @@ export class AddonCalendarEventPage { }); // Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them. - this.notificationFormat = this.timeUtils.convertPHPToMoment(this.translate.instant('core.strftimedatetimeshort')) + this.notificationFormat = this.timeUtils.convertPHPToMoment(this.translate.instant('core.strftimedatetime')) .replace(/[\[\]]/g, ''); } } diff --git a/src/addon/mod/data/fields/date/component/date.ts b/src/addon/mod/data/fields/date/component/date.ts index 5b17bf4a5ee..187a6fbba36 100644 --- a/src/addon/mod/data/fields/date/component/date.ts +++ b/src/addon/mod/data/fields/date/component/date.ts @@ -43,7 +43,7 @@ export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginCompo let val; // Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them. - this.format = this.timeUtils.convertPHPToMoment(this.translate.instant('core.strftimedatefullshort')) + this.format = this.timeUtils.convertPHPToMoment(this.translate.instant('core.strftimedate')) .replace(/[\[\]]/g, ''); if (this.mode == 'search') { diff --git a/src/addon/userprofilefield/datetime/component/datetime.ts b/src/addon/userprofilefield/datetime/component/datetime.ts index c43a61bc022..1d9e9ea036a 100644 --- a/src/addon/userprofilefield/datetime/component/datetime.ts +++ b/src/addon/userprofilefield/datetime/component/datetime.ts @@ -49,7 +49,7 @@ export class AddonUserProfileFieldDatetimeComponent implements OnInit { // Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them. field.format = this.timeUtils.convertPHPToMoment(this.translate.instant('core.' + - (hasTime ? 'strftimedatetimeshort' : 'strftimedatefullshort'))).replace(/[\[\]]/g, ''); + (hasTime ? 'strftimedatetime' : 'strftimedate'))).replace(/[\[\]]/g, ''); // Check min value. if (field.param1) { diff --git a/src/providers/lang.ts b/src/providers/lang.ts index 04f8283ee26..68463d4e34e 100644 --- a/src/providers/lang.ts +++ b/src/providers/lang.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import * as moment from 'moment'; import { Globalization } from '@ionic-native/globalization'; -import { Platform } from 'ionic-angular'; +import { Platform, Config } from 'ionic-angular'; import { CoreConfigProvider } from './config'; import { CoreConfigConstants } from '../configconstants'; @@ -33,7 +33,7 @@ export class CoreLangProvider { protected sitePluginsStrings = {}; // Strings defined by site plugins. constructor(private translate: TranslateService, private configProvider: CoreConfigProvider, platform: Platform, - private globalization: Globalization) { + private globalization: Globalization, private config: Config) { // Set fallback language and language to use until the app determines the right language to use. translate.setDefaultLang(this.fallbackLanguage); translate.use(this.defaultLanguage); @@ -86,6 +86,17 @@ export class CoreLangProvider { } } + /** + * Capitalize a string (make the first letter uppercase). + * We cannot use a function from text utils because it would cause a circular dependency. + * + * @param {string} value String to capitalize. + * @return {string} Capitalized string. + */ + protected capitalize(value: string): string { + return value.charAt(0).toUpperCase() + value.slice(1); + } + /** * Change current language. * @@ -142,6 +153,13 @@ export class CoreLangProvider { // Use british english when parent english is loaded. moment.locale(language == 'en' ? 'en-gb' : language); + + // Set data for ion-datetime. + this.config.set('monthNames', moment.months().map(this.capitalize.bind(this))); + this.config.set('monthShortNames', moment.monthsShort().map(this.capitalize.bind(this))); + this.config.set('dayNames', moment.weekdays().map(this.capitalize.bind(this))); + this.config.set('dayShortNames', moment.weekdaysShort().map(this.capitalize.bind(this))); + this.currentLanguage = language; return Promise.all(promises).finally(() => { From 5ed9ac0e3ef0ca72f0e226dce1498a3e85936a9c Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 8 Jul 2019 11:08:07 +0200 Subject: [PATCH 041/241] MOBILE-2991 settings: Sort languages by name --- src/core/settings/pages/general/general.html | 2 +- src/core/settings/pages/general/general.ts | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/core/settings/pages/general/general.html b/src/core/settings/pages/general/general.html index 2358295dedd..1ed053f9d19 100644 --- a/src/core/settings/pages/general/general.html +++ b/src/core/settings/pages/general/general.html @@ -7,7 +7,7 @@

{{ 'core.settings.language' | translate }}

- {{ languages[code] }} + {{ entry.name }}
diff --git a/src/core/settings/pages/general/general.ts b/src/core/settings/pages/general/general.ts index e6fbd925647..54aac1ca306 100644 --- a/src/core/settings/pages/general/general.ts +++ b/src/core/settings/pages/general/general.ts @@ -34,8 +34,7 @@ import { CoreConfigConstants } from '../../../../configconstants'; }) export class CoreSettingsGeneralPage { - languages = {}; - languageCodes = []; + languages = []; selectedLanguage: string; rteSupported: boolean; richTextEditor: boolean; @@ -46,8 +45,20 @@ export class CoreSettingsGeneralPage { private domUtils: CoreDomUtilsProvider, localNotificationsProvider: CoreLocalNotificationsProvider) { - this.languages = CoreConfigConstants.languages; - this.languageCodes = Object.keys(this.languages); + // Get the supported languages. + const languages = CoreConfigConstants.languages; + for (const code in languages) { + this.languages.push({ + code: code, + name: languages[code] + }); + } + + // Sort them by name. + this.languages.sort((a, b) => { + return a.name.localeCompare(b.name); + }); + langProvider.getCurrentLanguage().then((currentLanguage) => { this.selectedLanguage = currentLanguage; }); From 632a0dc57ac2a4322c9b0ba6284145a73f2c01fe Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 8 Jul 2019 15:41:14 +0200 Subject: [PATCH 042/241] MOBILE-3029 core: Let site plugins override NavController events --- .../components/compile-html/compile-html.ts | 52 +++++++++++++++++++ src/core/mainmenu/pages/more/more.html | 2 +- .../components/module-index/module-index.ts | 11 ++++ .../plugin-content/plugin-content.ts | 13 +++++ .../pages/module-index/module-index.ts | 44 ++++++++++++++++ .../pages/plugin-page/plugin-page.ts | 44 ++++++++++++++++ 6 files changed, 165 insertions(+), 1 deletion(-) diff --git a/src/core/compile/components/compile-html/compile-html.ts b/src/core/compile/components/compile-html/compile-html.ts index a263241d9e9..82c52cd8ebb 100644 --- a/src/core/compile/components/compile-html/compile-html.ts +++ b/src/core/compile/components/compile-html/compile-html.ts @@ -60,6 +60,7 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { protected element; protected differ: any; // To detect changes in the jsData input. protected creatingComponent = false; + protected pendingCalls = {}; constructor(protected compileProvider: CoreCompileProvider, protected cdr: ChangeDetectorRef, element: ElementRef, @Optional() protected navCtrl: NavController, differs: KeyValueDiffers, protected domUtils: CoreDomUtilsProvider, @@ -165,6 +166,22 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { if (compileInstance.javascript) { compileInstance.compileProvider.executeJavascript(this, compileInstance.javascript); } + + // Call the pending functions. + for (const name in compileInstance.pendingCalls) { + const pendingCall = compileInstance.pendingCalls[name]; + + if (typeof this[name] == 'function') { + // Call the function. + Promise.resolve(this[name].apply(this, pendingCall.params)).then(pendingCall.defer.resolve) + .catch(pendingCall.defer.reject); + } else { + // Function not defined, resolve the promise. + pendingCall.defer.resolve(); + } + } + + compileInstance.pendingCalls = {}; } /** @@ -200,4 +217,39 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { } } } + + /** + * Call a certain function on the component instance. + * + * @param {string} name Name of the function to call. + * @param {any[]} params List of params to send to the function. + * @param {boolean} [callWhenCreated=true] If this param is true and the component hasn't been created yet, call the function + * once the component has been created. + * @return {any} Result of the call. Undefined if no component instance or the function doesn't exist. + */ + callComponentFunction(name: string, params?: any[], callWhenCreated: boolean = true): any { + if (this.componentInstance) { + if (typeof this.componentInstance[name] == 'function') { + return this.componentInstance[name].apply(this.componentInstance, params); + } + } else if (callWhenCreated) { + // Call it when the component is created. + + if (this.pendingCalls[name]) { + // Call already pending, just update the params (allow only 1 call per function until it's initialized). + this.pendingCalls[name].params = params; + + return this.pendingCalls[name].defer.promise; + } + + const defer = this.utils.promiseDefer(); + + this.pendingCalls[name] = { + params: params, + defer: defer + }; + + return defer.promise; + } + } } diff --git a/src/core/mainmenu/pages/more/more.html b/src/core/mainmenu/pages/more/more.html index e723bc44af7..60ccc28a4b4 100644 --- a/src/core/mainmenu/pages/more/more.html +++ b/src/core/mainmenu/pages/more/more.html @@ -13,7 +13,7 @@ - +

{{ handler.title | translate}}

{{handler.badge}} diff --git a/src/core/siteplugins/components/module-index/module-index.ts b/src/core/siteplugins/components/module-index/module-index.ts index 3e231ecf444..79be3063c5a 100644 --- a/src/core/siteplugins/components/module-index/module-index.ts +++ b/src/core/siteplugins/components/module-index/module-index.ts @@ -169,4 +169,15 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C this.isDestroyed = true; this.statusObserver && this.statusObserver.off(); } + + /** + * Call a certain function on the component instance. + * + * @param {string} name Name of the function to call. + * @param {any[]} params List of params to send to the function. + * @return {any} Result of the call. Undefined if no component instance or the function doesn't exist. + */ + callComponentFunction(name: string, params?: any[]): any { + return this.content.callComponentFunction(name, params); + } } diff --git a/src/core/siteplugins/components/plugin-content/plugin-content.ts b/src/core/siteplugins/components/plugin-content/plugin-content.ts index e1fa41f1787..4b635684456 100644 --- a/src/core/siteplugins/components/plugin-content/plugin-content.ts +++ b/src/core/siteplugins/components/plugin-content/plugin-content.ts @@ -176,4 +176,17 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck { this.fetchContent(); } + + /** + * Call a certain function on the component instance. + * + * @param {string} name Name of the function to call. + * @param {any[]} params List of params to send to the function. + * @return {any} Result of the call. Undefined if no component instance or the function doesn't exist. + */ + callComponentFunction(name: string, params?: any[]): any { + if (this.compileComponent) { + return ( this.compileComponent).callComponentFunction(name, params); + } + } } diff --git a/src/core/siteplugins/pages/module-index/module-index.ts b/src/core/siteplugins/pages/module-index/module-index.ts index de4050eb337..f5829666c12 100644 --- a/src/core/siteplugins/pages/module-index/module-index.ts +++ b/src/core/siteplugins/pages/module-index/module-index.ts @@ -48,4 +48,48 @@ export class CoreSitePluginsModuleIndexPage { refresher.complete(); }); } + + /** + * The page is about to enter and become the active page. + */ + ionViewWillEnter(): void { + this.content.callComponentFunction('ionViewWillEnter'); + } + + /** + * The page has fully entered and is now the active page. This event will fire, whether it was the first load or a cached page. + */ + ionViewDidEnter(): void { + this.content.callComponentFunction('ionViewDidEnter'); + } + + /** + * The page is about to leave and no longer be the active page. + */ + ionViewWillLeave(): void { + this.content.callComponentFunction('ionViewWillLeave'); + } + + /** + * The page has finished leaving and is no longer the active page. + */ + ionViewDidLeave(): void { + this.content.callComponentFunction('ionViewDidLeave'); + } + + /** + * The page is about to be destroyed and have its elements removed. + */ + ionViewWillUnload(): void { + this.content.callComponentFunction('ionViewWillUnload'); + } + + /** + * Check if we can leave the page or not. + * + * @return {boolean|Promise} Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): boolean | Promise { + return this.content.callComponentFunction('ionViewCanLeave'); + } } diff --git a/src/core/siteplugins/pages/plugin-page/plugin-page.ts b/src/core/siteplugins/pages/plugin-page/plugin-page.ts index 4e18eef73a0..4d3128918bd 100644 --- a/src/core/siteplugins/pages/plugin-page/plugin-page.ts +++ b/src/core/siteplugins/pages/plugin-page/plugin-page.ts @@ -56,4 +56,48 @@ export class CoreSitePluginsPluginPage { refresher.complete(); }); } + + /** + * The page is about to enter and become the active page. + */ + ionViewWillEnter(): void { + this.content.callComponentFunction('ionViewWillEnter'); + } + + /** + * The page has fully entered and is now the active page. This event will fire, whether it was the first load or a cached page. + */ + ionViewDidEnter(): void { + this.content.callComponentFunction('ionViewDidEnter'); + } + + /** + * The page is about to leave and no longer be the active page. + */ + ionViewWillLeave(): void { + this.content.callComponentFunction('ionViewWillLeave'); + } + + /** + * The page has finished leaving and is no longer the active page. + */ + ionViewDidLeave(): void { + this.content.callComponentFunction('ionViewDidLeave'); + } + + /** + * The page is about to be destroyed and have its elements removed. + */ + ionViewWillUnload(): void { + this.content.callComponentFunction('ionViewWillUnload'); + } + + /** + * Check if we can leave the page or not. + * + * @return {boolean|Promise} Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): boolean | Promise { + return this.content.callComponentFunction('ionViewCanLeave'); + } } From db0fe2052a4764b34ce463388ec40217ed6ac2bb Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 9 Jul 2019 12:00:45 +0200 Subject: [PATCH 043/241] MOBILE-3043 lesson: Improve error message when viewing old retakes --- scripts/langindex.json | 1 + .../lesson/pages/user-retake/user-retake.ts | 11 +++++-- src/assets/lang/en.json | 1 + src/lang/en.json | 1 + src/providers/utils/utils.ts | 29 ++++++++++++++++++- 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 7381c6fc8b7..c9808298f2a 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1383,6 +1383,7 @@ "core.erroropenfilenoextension": "local_moodlemobileapp", "core.erroropenpopup": "local_moodlemobileapp", "core.errorrenamefile": "local_moodlemobileapp", + "core.errorsomedatanotdownloaded": "local_moodlemobileapp", "core.errorsync": "local_moodlemobileapp", "core.errorsyncblocked": "local_moodlemobileapp", "core.explanationdigitalminor": "moodle", diff --git a/src/addon/mod/lesson/pages/user-retake/user-retake.ts b/src/addon/mod/lesson/pages/user-retake/user-retake.ts index 8a69c73ded2..394f781761a 100644 --- a/src/addon/mod/lesson/pages/user-retake/user-retake.ts +++ b/src/addon/mod/lesson/pages/user-retake/user-retake.ts @@ -19,6 +19,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUserProvider } from '@core/user/providers/user'; import { AddonModLessonProvider } from '../../providers/lesson'; import { AddonModLessonHelperProvider } from '../../providers/helper'; @@ -44,11 +45,13 @@ export class AddonModLessonUserRetakePage implements OnInit { protected lessonId: number; // The lesson ID the retake belongs to. protected userId: number; // User ID to see the retakes. protected retakeNumber: number; // Number of the initial retake to see. + protected previousSelectedRetake: number; // To be able to detect the previous selected retake when it has changed. constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, protected textUtils: CoreTextUtilsProvider, protected translate: TranslateService, protected domUtils: CoreDomUtilsProvider, protected userProvider: CoreUserProvider, protected timeUtils: CoreTimeUtilsProvider, - protected lessonProvider: AddonModLessonProvider, protected lessonHelper: AddonModLessonHelperProvider) { + protected lessonProvider: AddonModLessonProvider, protected lessonHelper: AddonModLessonHelperProvider, + protected utils: CoreUtilsProvider) { this.lessonId = navParams.get('lessonId'); this.courseId = navParams.get('courseId'); @@ -75,7 +78,8 @@ export class AddonModLessonUserRetakePage implements OnInit { this.loaded = false; this.setRetake(retakeNumber).catch((error) => { - this.domUtils.showErrorModalDefault(error, 'Error getting attempt.'); + this.selectedRetake = this.previousSelectedRetake; + this.domUtils.showErrorModal(this.utils.addDataNotDownloadedError(error, 'Error getting attempt.')); }).finally(() => { this.loaded = true; }); @@ -128,7 +132,7 @@ export class AddonModLessonUserRetakePage implements OnInit { student.bestgrade = this.textUtils.roundToDecimals(student.bestgrade, 2); student.attempts.forEach((retake) => { - if (this.retakeNumber == retake.try) { + if (!this.selectedRetake && this.retakeNumber == retake.try) { // The retake specified as parameter exists. Use it. this.selectedRetake = this.retakeNumber; } @@ -223,6 +227,7 @@ export class AddonModLessonUserRetakePage implements OnInit { } this.retake = data; + this.previousSelectedRetake = this.selectedRetake; }); } } diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 7ed0d48128f..4730a84480f 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1383,6 +1383,7 @@ "core.erroropenfilenoextension": "Error opening file: the file doesn't have an extension.", "core.erroropenpopup": "This activity is trying to open a popup. This is not supported in the app.", "core.errorrenamefile": "Error renaming file. Please try again.", + "core.errorsomedatanotdownloaded": "If you downloaded this activity, please notice that some data isn't downloaded during the download process for performance and data usage reasons.", "core.errorsync": "An error occurred while synchronising. Please try again.", "core.errorsyncblocked": "This {{$a}} cannot be synchronised right now because of an ongoing process. Please try again later. If the problem persists, try restarting the app.", "core.explanationdigitalminor": "This information is required to determine if your age is over the digital age of consent. This is the age when an individual can consent to terms and conditions and their data being legally stored and processed.", diff --git a/src/lang/en.json b/src/lang/en.json index bf2f305e543..d780ed3af2f 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -91,6 +91,7 @@ "erroropenfilenoextension": "Error opening file: the file doesn't have an extension.", "erroropenpopup": "This activity is trying to open a popup. This is not supported in the app.", "errorrenamefile": "Error renaming file. Please try again.", + "errorsomedatanotdownloaded": "If you downloaded this activity, please notice that some data isn't downloaded during the download process for performance and data usage reasons.", "errorsync": "An error occurred while synchronising. Please try again.", "errorsyncblocked": "This {{$a}} cannot be synchronised right now because of an ongoing process. Please try again later. If the problem persists, try restarting the app.", "explanationdigitalminor": "This information is required to determine if your age is over the digital age of consent. This is the age when an individual can consent to terms and conditions and their data being legally stored and processed.", diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index e1595c75ba9..77867c5007d 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -21,6 +21,7 @@ import { WebIntent } from '@ionic-native/web-intent'; import { CoreAppProvider } from '../app'; import { CoreDomUtilsProvider } from './dom'; import { CoreMimetypeUtilsProvider } from './mimetype'; +import { CoreTextUtilsProvider } from './text'; import { CoreEventsProvider } from '../events'; import { CoreLoggerProvider } from '../logger'; import { TranslateService } from '@ngx-translate/core'; @@ -66,10 +67,36 @@ export class CoreUtilsProvider { private domUtils: CoreDomUtilsProvider, logger: CoreLoggerProvider, private translate: TranslateService, private platform: Platform, private langProvider: CoreLangProvider, private eventsProvider: CoreEventsProvider, private fileOpener: FileOpener, private mimetypeUtils: CoreMimetypeUtilsProvider, private webIntent: WebIntent, - private wsProvider: CoreWSProvider, private zone: NgZone) { + private wsProvider: CoreWSProvider, private zone: NgZone, private textUtils: CoreTextUtilsProvider) { this.logger = logger.getInstance('CoreUtilsProvider'); } + /** + * Given an error, add an extra warning to the error message and return the new error message. + * + * @param {any} error Error object or message. + * @param {any} [defaultError] Message to show if the error is not a string. + * @return {string} New error message. + */ + addDataNotDownloadedError(error: any, defaultError?: string): string { + let errorMessage = error; + + if (error && typeof error != 'string') { + errorMessage = this.textUtils.getErrorMessageFromError(error); + } + + if (typeof errorMessage != 'string') { + errorMessage = defaultError || ''; + } + + if (!this.isWebServiceError(error)) { + // Local error. Add an extra warning. + errorMessage += '

' + this.translate.instant('core.errorsomedatanotdownloaded'); + } + + return errorMessage; + } + /** * Similar to Promise.all, but if a promise fails this function's promise won't be rejected until ALL promises have finished. * From efc359cafdfe3aeb8f10d99fbad9f5737208e169 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 9 Jul 2019 15:18:56 +0200 Subject: [PATCH 044/241] MOBILE-3067 notifications: Fix mark read when it shouldn't --- src/addon/notifications/pages/list/list.ts | 48 ++++++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/src/addon/notifications/pages/list/list.ts b/src/addon/notifications/pages/list/list.ts index 3b8d1751ad1..1ab97b2be9f 100644 --- a/src/addon/notifications/pages/list/list.ts +++ b/src/addon/notifications/pages/list/list.ts @@ -41,8 +41,10 @@ export class AddonNotificationsListPage { canMarkAllNotificationsAsRead = false; loadingMarkAllNotificationsAsRead = false; + protected isCurrentView: boolean; protected cronObserver: CoreEventObserver; protected pushObserver: Subscription; + protected pendingRefresh = false; constructor(navParams: NavParams, private domUtils: CoreDomUtilsProvider, private eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, @@ -55,17 +57,24 @@ export class AddonNotificationsListPage { * View loaded. */ ionViewDidLoad(): void { - this.fetchNotifications().finally(() => { - this.notificationsLoaded = true; - }); + this.fetchNotifications(); - this.cronObserver = this.eventsProvider.on(AddonNotificationsProvider.READ_CRON_EVENT, () => this.refreshNotifications(), - this.sitesProvider.getCurrentSiteId()); + this.cronObserver = this.eventsProvider.on(AddonNotificationsProvider.READ_CRON_EVENT, () => { + if (this.isCurrentView) { + this.notificationsLoaded = false; + this.refreshNotifications(); + } + }, this.sitesProvider.getCurrentSiteId()); this.pushObserver = this.pushNotificationsDelegate.on('receive').subscribe((notification) => { // New notification received. If it's from current site, refresh the data. - if (this.utils.isTrueOrOne(notification.notif) && this.sitesProvider.isCurrentSite(notification.site)) { + if (this.isCurrentView && this.utils.isTrueOrOne(notification.notif) && + this.sitesProvider.isCurrentSite(notification.site)) { + + this.notificationsLoaded = false; this.refreshNotifications(); + } else if (!this.isCurrentView) { + this.pendingRefresh = true; } }); } @@ -93,6 +102,8 @@ export class AddonNotificationsListPage { }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.notifications.errorgetnotifications', true); this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. + }).finally(() => { + this.notificationsLoaded = true; }); } @@ -110,9 +121,7 @@ export class AddonNotificationsListPage { // All marked as read, refresh the list. this.notificationsLoaded = false; - return this.refreshNotifications().finally(() => { - this.notificationsLoaded = true; - }); + return this.refreshNotifications(); }); } @@ -198,6 +207,27 @@ export class AddonNotificationsListPage { notification.mobiletext = this.textUtils.replaceNewLines(text, '
'); } + /** + * User entered the page. + */ + ionViewDidEnter(): void { + this.isCurrentView = true; + + if (this.pendingRefresh) { + this.pendingRefresh = false; + this.notificationsLoaded = false; + + this.refreshNotifications(); + } + } + + /** + * User left the page. + */ + ionViewDidLeave(): void { + this.isCurrentView = false; + } + /** * Page destroyed. */ From 0b779f4ae84b10d918fd4ece678f39de63faff7c Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 10 Jul 2019 09:52:05 +0200 Subject: [PATCH 045/241] MOBILE-3071 core: Let site plugins add menu items in course --- .../blog/providers/course-option-handler.ts | 4 +- .../providers/course-option-handler.ts | 4 +- .../providers/coursemenu-handler.ts | 6 ++- src/core/course/providers/options-delegate.ts | 8 ++-- .../grades/providers/course-option-handler.ts | 4 +- .../classes/handlers/course-option-handler.ts | 39 +++++++++++++++++-- .../user/providers/course-option-handler.ts | 4 +- 7 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/addon/blog/providers/course-option-handler.ts b/src/addon/blog/providers/course-option-handler.ts index 9c28973fb2f..5617d6c9258 100644 --- a/src/addon/blog/providers/course-option-handler.ts +++ b/src/addon/blog/providers/course-option-handler.ts @@ -78,10 +78,10 @@ export class AddonBlogCourseOptionHandler implements CoreCourseOptionsHandler { * Returns the data needed to render the handler. * * @param {Injector} injector Injector. - * @param {number} courseId The course ID. + * @param {number} course The course. * @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise { + getDisplayData(injector: Injector, course: any): CoreCourseOptionsHandlerData | Promise { return { title: 'addon.blog.blog', class: 'addon-blog-handler', diff --git a/src/addon/competency/providers/course-option-handler.ts b/src/addon/competency/providers/course-option-handler.ts index f05219300ee..2f817699504 100644 --- a/src/addon/competency/providers/course-option-handler.ts +++ b/src/addon/competency/providers/course-option-handler.ts @@ -63,10 +63,10 @@ export class AddonCompetencyCourseOptionHandler implements CoreCourseOptionsHand * Returns the data needed to render the handler. * * @param {Injector} injector Injector. - * @param {number} courseId The course ID. + * @param {number} course The course. * @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData?(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise { + getDisplayData?(injector: Injector, course: any): CoreCourseOptionsHandlerData | Promise { return { title: 'addon.competency.competencies', class: 'addon-competency-course-handler', diff --git a/src/addon/storagemanager/providers/coursemenu-handler.ts b/src/addon/storagemanager/providers/coursemenu-handler.ts index e2aad3def21..69a578fe08d 100644 --- a/src/addon/storagemanager/providers/coursemenu-handler.ts +++ b/src/addon/storagemanager/providers/coursemenu-handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreCourseOptionsMenuHandler, CoreCourseOptionsMenuHandlerData } from '@core/course/providers/options-delegate'; /** @@ -49,9 +49,11 @@ export class AddonStorageManagerCourseMenuHandler implements CoreCourseOptionsMe /** * Returns the data needed to render the handler. * + * @param {Injector} injector Injector. + * @param {any} course The course. * @return {CoreCourseOptionsMenuHandlerData} Data needed to render the handler. */ - getMenuDisplayData(): CoreCourseOptionsMenuHandlerData { + getMenuDisplayData(injector: Injector, course: any): CoreCourseOptionsMenuHandlerData { return { icon: 'cube', title: 'addon.storagemanager.managestorage', diff --git a/src/core/course/providers/options-delegate.ts b/src/core/course/providers/options-delegate.ts index cccfde28172..b99cc776005 100644 --- a/src/core/course/providers/options-delegate.ts +++ b/src/core/course/providers/options-delegate.ts @@ -52,10 +52,10 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler { * Returns the data needed to render the handler. * * @param {Injector} injector Injector. - * @param {number} courseId The course ID. + * @param {number} course The course. * @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData?(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise; + getDisplayData?(injector: Injector, course: any): CoreCourseOptionsHandlerData | Promise; /** * Should invalidate the data to determine if the handler is enabled for a certain course. @@ -84,10 +84,10 @@ export interface CoreCourseOptionsMenuHandler extends CoreCourseOptionsHandler { * Returns the data needed to render the handler. * * @param {Injector} injector Injector. - * @param {number} courseId The course ID. + * @param {number} course The course. * @return {CoreCourseOptionsMenuHandlerData|Promise} Data or promise resolved with data. */ - getMenuDisplayData(injector: Injector, courseId: number): + getMenuDisplayData(injector: Injector, course: any): CoreCourseOptionsMenuHandlerData | Promise; } diff --git a/src/core/grades/providers/course-option-handler.ts b/src/core/grades/providers/course-option-handler.ts index ae04b557478..d544e4e856a 100644 --- a/src/core/grades/providers/course-option-handler.ts +++ b/src/core/grades/providers/course-option-handler.ts @@ -80,10 +80,10 @@ export class CoreGradesCourseOptionHandler implements CoreCourseOptionsHandler { * Returns the data needed to render the handler. * * @param {Injector} injector Injector. - * @param {number} courseId The course ID. + * @param {number} course The course. * @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise { + getDisplayData(injector: Injector, course: any): CoreCourseOptionsHandlerData | Promise { return { title: 'core.grades.grades', class: 'core-grades-course-handler', diff --git a/src/core/siteplugins/classes/handlers/course-option-handler.ts b/src/core/siteplugins/classes/handlers/course-option-handler.ts index 0ecaa751543..e918ecf115c 100644 --- a/src/core/siteplugins/classes/handlers/course-option-handler.ts +++ b/src/core/siteplugins/classes/handlers/course-option-handler.ts @@ -14,7 +14,9 @@ import { Injector } from '@angular/core'; import { CoreSitePluginsProvider } from '../../providers/siteplugins'; -import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '@core/course/providers/options-delegate'; +import { + CoreCourseOptionsHandler, CoreCourseOptionsHandlerData, CoreCourseOptionsMenuHandlerData +} from '@core/course/providers/options-delegate'; import { CoreSitePluginsBaseHandler } from './base-handler'; import { CoreSitePluginsCourseOptionComponent } from '../../components/course-option/course-option'; @@ -23,12 +25,14 @@ import { CoreSitePluginsCourseOptionComponent } from '../../components/course-op */ export class CoreSitePluginsCourseOptionHandler extends CoreSitePluginsBaseHandler implements CoreCourseOptionsHandler { priority: number; + isMenuHandler: boolean; constructor(name: string, protected title: string, protected plugin: any, protected handlerSchema: any, protected initResult: any, protected sitePluginsProvider: CoreSitePluginsProvider) { super(name); this.priority = handlerSchema.priority; + this.isMenuHandler = !!handlerSchema.ismenuhandler; } /** @@ -46,13 +50,13 @@ export class CoreSitePluginsCourseOptionHandler extends CoreSitePluginsBaseHandl } /** - * Returns the data needed to render the handler. + * Returns the data needed to render the handler (if it isn't a menu handler). * * @param {Injector} injector Injector. - * @param {number} courseId The course ID. + * @param {number} course The course. * @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise { + getDisplayData(injector: Injector, course: any): CoreCourseOptionsHandlerData | Promise { return { title: this.title, class: this.handlerSchema.displaydata.class, @@ -63,6 +67,33 @@ export class CoreSitePluginsCourseOptionHandler extends CoreSitePluginsBaseHandl }; } + /** + * Returns the data needed to render the handler (if it's a menu handler). + * + * @param {Injector} injector Injector. + * @param {any} course The course. + * @return {CoreCourseOptionsMenuHandlerData|Promise} Data or promise resolved with data. + */ + getMenuDisplayData(injector: Injector, course: any): + CoreCourseOptionsMenuHandlerData | Promise { + + return { + title: this.title, + class: this.handlerSchema.displaydata.class, + icon: this.handlerSchema.displaydata.icon || '', + page: 'CoreSitePluginsPluginPage', + pageParams: { + title: this.title, + component: this.plugin.component, + method: this.handlerSchema.method, + args: { + courseid: course.id + }, + initResult: this.initResult + } + }; + } + /** * Called when a course is downloaded. It should prefetch all the data to be able to see the plugin in offline. * diff --git a/src/core/user/providers/course-option-handler.ts b/src/core/user/providers/course-option-handler.ts index 4f91bb0ff49..9636dcfeb27 100644 --- a/src/core/user/providers/course-option-handler.ts +++ b/src/core/user/providers/course-option-handler.ts @@ -79,10 +79,10 @@ export class CoreUserParticipantsCourseOptionHandler implements CoreCourseOption * Returns the data needed to render the handler. * * @param {Injector} injector Injector. - * @param {number} courseId The course ID. + * @param {number} course The course. * @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data. */ - getDisplayData(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise { + getDisplayData(injector: Injector, course: any): CoreCourseOptionsHandlerData | Promise { return { title: 'core.user.participants', class: 'core-user-participants-handler', From d44100f757301ef90030e54c5963bc45f1a42cfc Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 8 Jul 2019 11:39:07 +0200 Subject: [PATCH 046/241] MOBILE-2941 login: Display forgot password button in reconnect --- .../login/pages/credentials/credentials.ts | 24 ++------------ src/core/login/pages/reconnect/reconnect.html | 5 +++ src/core/login/pages/reconnect/reconnect.ts | 7 ++++ src/core/login/providers/helper.ts | 32 +++++++++++++++++++ 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/src/core/login/pages/credentials/credentials.ts b/src/core/login/pages/credentials/credentials.ts index 20b3d2116dc..3c208919085 100644 --- a/src/core/login/pages/credentials/credentials.ts +++ b/src/core/login/pages/credentials/credentials.ts @@ -19,7 +19,6 @@ import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreLoginHelperProvider } from '../../providers/helper'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { CoreConfigConstants } from '../../../../configconstants'; @@ -53,7 +52,7 @@ export class CoreLoginCredentialsPage { constructor(private navCtrl: NavController, navParams: NavParams, fb: FormBuilder, private appProvider: CoreAppProvider, private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider, - private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private utils: CoreUtilsProvider, + private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private eventsProvider: CoreEventsProvider) { this.siteUrl = navParams.get('siteUrl'); @@ -230,26 +229,7 @@ export class CoreLoginCredentialsPage { * Forgotten password button clicked. */ forgottenPassword(): void { - if (this.siteConfig && this.siteConfig.forgottenpasswordurl) { - // URL set, open it. - this.utils.openInApp(this.siteConfig.forgottenpasswordurl); - - return; - } - - // Check if password reset can be done through the app. - const modal = this.domUtils.showModalLoading(); - this.loginHelper.canRequestPasswordReset(this.siteUrl).then((canReset) => { - if (canReset) { - this.navCtrl.push('CoreLoginForgottenPasswordPage', { - siteUrl: this.siteUrl, username: this.credForm.value.username - }); - } else { - this.loginHelper.openForgottenPassword(this.siteUrl); - } - }).finally(() => { - modal.dismiss(); - }); + this.loginHelper.forgottenPasswordClicked(this.navCtrl, this.siteUrl, this.credForm.value.username, this.siteConfig); } /** diff --git a/src/core/login/pages/reconnect/reconnect.html b/src/core/login/pages/reconnect/reconnect.html index ce0edcde673..7d4cadb3711 100644 --- a/src/core/login/pages/reconnect/reconnect.html +++ b/src/core/login/pages/reconnect/reconnect.html @@ -49,6 +49,11 @@ + +
+ +
+ {{ 'core.login.potentialidps' | translate }} diff --git a/src/core/login/pages/reconnect/reconnect.ts b/src/core/login/pages/reconnect/reconnect.ts index 4114678c4d9..b7882e1f11b 100644 --- a/src/core/login/pages/reconnect/reconnect.ts +++ b/src/core/login/pages/reconnect/reconnect.ts @@ -161,6 +161,13 @@ export class CoreLoginReconnectPage { }); } + /** + * Forgotten password button clicked. + */ + forgottenPassword(): void { + this.loginHelper.forgottenPasswordClicked(this.navCtrl, this.siteUrl, this.credForm.value.username, this.siteConfig); + } + /** * An OAuth button was clicked. * diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index 2d4bbc62f61..5adc9ac328d 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -260,6 +260,38 @@ export class CoreLoginHelperProvider { }); } + /** + * Helper function to act when the forgotten password is clicked. + * + * @param {NavController} navCtrl NavController to use to navigate. + * @param {string} siteUrl Site URL. + * @param {string} username Username. + * @param {any} [siteConfig] Site config. + */ + forgottenPasswordClicked(navCtrl: NavController, siteUrl: string, username: string, siteConfig?: any): void { + if (siteConfig && siteConfig.forgottenpasswordurl) { + // URL set, open it. + this.utils.openInApp(siteConfig.forgottenpasswordurl); + + return; + } + + // Check if password reset can be done through the app. + const modal = this.domUtils.showModalLoading(); + + this.canRequestPasswordReset(siteUrl).then((canReset) => { + if (canReset) { + navCtrl.push('CoreLoginForgottenPasswordPage', { + siteUrl: siteUrl, username: username + }); + } else { + this.openForgottenPassword(siteUrl); + } + }).finally(() => { + modal.dismiss(); + }); + } + /** * Format profile fields, filtering the ones that shouldn't be shown on signup and classifying them in categories. * From 8f8efe4052e5d738ac46646c02bfbc46bb91bbcd Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 15 Jul 2019 11:38:52 +0200 Subject: [PATCH 047/241] MOBILE-3072 siteplugins: Add version to CSS URL --- src/core/siteplugins/providers/helper.ts | 5 +++++ src/providers/filepool.ts | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/core/siteplugins/providers/helper.ts b/src/core/siteplugins/providers/helper.ts index b553787286e..2b1242d85aa 100644 --- a/src/core/siteplugins/providers/helper.ts +++ b/src/core/siteplugins/providers/helper.ts @@ -142,6 +142,11 @@ export class CoreSitePluginsHelperProvider { url = this.textUtils.concatenatePaths(site.getURL(), url); } + if (url && handlerSchema.styles.version) { + // Add the version to the URL to prevent getting a cached file. + url += (url.indexOf('?') != -1 ? '&' : '?') + 'version=' + handlerSchema.styles.version; + } + const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), componentId = uniqueName + '#main'; diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts index 09d873f9478..aded3de7e83 100644 --- a/src/providers/filepool.ts +++ b/src/providers/filepool.ts @@ -2197,9 +2197,27 @@ export class CoreFilepoolProvider { filename = this.urlUtils.getLastFileWithoutParams(fileUrl); } + // If there are hashes in the URL, extract them. + const index = filename.indexOf('#'); + let hashes; + + if (index != -1) { + hashes = filename.split('#'); + + // Remove the URL from the array. + hashes.shift(); + + filename = filename.substr(0, index); + } + // Remove the extension from the filename. filename = this.mimeUtils.removeExtension(filename); + if (hashes) { + // Add hashes to the name. + filename += '_' + hashes.join('_'); + } + return this.textUtils.removeSpecialCharactersForFiles(filename); } From 8d64fea2afacf8c19f47e71704d8667d1e66aa96 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 16 Jul 2019 08:22:58 +0200 Subject: [PATCH 048/241] MOBILE-3098 iframe: Open in app links inside iframes --- src/components/iframe/iframe.ts | 13 ++++++-- .../siteplugins/components/block/block.ts | 2 +- src/directives/format-text.ts | 21 +++++++----- src/providers/utils/iframe.ts | 33 +++++++++++-------- 4 files changed, 43 insertions(+), 26 deletions(-) diff --git a/src/components/iframe/iframe.ts b/src/components/iframe/iframe.ts index 5988289a735..722d9eae633 100644 --- a/src/components/iframe/iframe.ts +++ b/src/components/iframe/iframe.ts @@ -12,11 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, Output, OnInit, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange } from '@angular/core'; +import { + Component, Input, Output, OnInit, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange, Optional +} from '@angular/core'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; +import { NavController } from 'ionic-angular'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreIframeUtilsProvider } from '@providers/utils/iframe'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; /** */ @@ -38,7 +42,9 @@ export class CoreIframeComponent implements OnInit, OnChanges { protected IFRAME_TIMEOUT = 15000; constructor(logger: CoreLoggerProvider, private iframeUtils: CoreIframeUtilsProvider, private domUtils: CoreDomUtilsProvider, - private sanitizer: DomSanitizer) { + private sanitizer: DomSanitizer, private navCtrl: NavController, + @Optional() private svComponent: CoreSplitViewComponent) { + this.logger = logger.getInstance('CoreIframe'); this.loaded = new EventEmitter(); } @@ -55,7 +61,8 @@ export class CoreIframeComponent implements OnInit, OnChanges { // Show loading only with external URLs. this.loading = !this.src || !!this.src.match(/^https?:\/\//i); - this.iframeUtils.treatFrame(iframe); + const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; + this.iframeUtils.treatFrame(iframe, false, navCtrl); if (this.loading) { iframe.addEventListener('load', () => { diff --git a/src/core/siteplugins/components/block/block.ts b/src/core/siteplugins/components/block/block.ts index d1e927add8d..807bceba388 100644 --- a/src/core/siteplugins/components/block/block.ts +++ b/src/core/siteplugins/components/block/block.ts @@ -27,7 +27,7 @@ import { CoreBlockDelegate } from '@core/block/providers/delegate'; }) export class CoreSitePluginsBlockComponent extends CoreBlockBaseComponent implements OnChanges { @Input() block: any; - @Input() contextLevel: number; + @Input() contextLevel: string; @Input() instanceId: number; @ViewChild(CoreSitePluginsPluginContentComponent) content: CoreSitePluginsPluginContentComponent; diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index 8cfbd306740..489561674e9 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -352,7 +352,8 @@ export class CoreFormatTextDirective implements OnChanges { this.utils.isTrueOrOne(this.singleLine), undefined, this.highlight); }).then((formatted) => { const div = document.createElement('div'), - canTreatVimeo = site && site.isVersionGreaterEqualThan(['3.3.4', '3.4']); + canTreatVimeo = site && site.isVersionGreaterEqualThan(['3.3.4', '3.4']), + navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; let images, anchors, audios, @@ -405,12 +406,12 @@ export class CoreFormatTextDirective implements OnChanges { }); videos.forEach((video) => { - this.treatVideoFilters(video); + this.treatVideoFilters(video, navCtrl); this.treatMedia(video); }); iframes.forEach((iframe) => { - this.treatIframe(iframe, site, canTreatVimeo); + this.treatIframe(iframe, site, canTreatVimeo, navCtrl); }); // Handle buttons with inner links. @@ -439,7 +440,7 @@ export class CoreFormatTextDirective implements OnChanges { // Handle all kind of frames. frames.forEach((frame: any) => { - this.iframeUtils.treatFrame(frame); + this.iframeUtils.treatFrame(frame, false, navCtrl); }); this.domUtils.handleBootstrapTooltips(div); @@ -508,8 +509,9 @@ export class CoreFormatTextDirective implements OnChanges { * Treat video filters. Currently only treating youtube video using video JS. * * @param {HTMLElement} el Video element. + * @param {NavController} navCtrl NavController to use. */ - protected treatVideoFilters(video: HTMLElement): void { + protected treatVideoFilters(video: HTMLElement, navCtrl: NavController): void { // Treat Video JS Youtube video links and translate them to iframes. if (!video.classList.contains('video-js')) { return; @@ -534,7 +536,7 @@ export class CoreFormatTextDirective implements OnChanges { // Replace video tag by the iframe. video.parentNode.replaceChild(iframe, video); - this.iframeUtils.treatFrame(iframe); + this.iframeUtils.treatFrame(iframe, false, navCtrl); } /** @@ -571,8 +573,9 @@ export class CoreFormatTextDirective implements OnChanges { * @param {HTMLIFrameElement} iframe Iframe to treat. * @param {CoreSite} site Site instance. * @param {boolean} canTreatVimeo Whether Vimeo videos can be treated in the site. + * @param {NavController} navCtrl NavController to use. */ - protected treatIframe(iframe: HTMLIFrameElement, site: CoreSite, canTreatVimeo: boolean): void { + protected treatIframe(iframe: HTMLIFrameElement, site: CoreSite, canTreatVimeo: boolean, navCtrl: NavController): void { const src = iframe.src, currentSite = this.sitesProvider.getCurrentSite(); @@ -583,7 +586,7 @@ export class CoreFormatTextDirective implements OnChanges { currentSite.getAutoLoginUrl(src, false).then((finalUrl) => { iframe.src = finalUrl; - this.iframeUtils.treatFrame(iframe); + this.iframeUtils.treatFrame(iframe, false, navCtrl); }); return; @@ -644,7 +647,7 @@ export class CoreFormatTextDirective implements OnChanges { } } - this.iframeUtils.treatFrame(iframe); + this.iframeUtils.treatFrame(iframe, false, navCtrl); } /** diff --git a/src/providers/utils/iframe.ts b/src/providers/utils/iframe.ts index 6771a6ed124..2952c22b1c6 100644 --- a/src/providers/utils/iframe.ts +++ b/src/providers/utils/iframe.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable, NgZone } from '@angular/core'; -import { Config, Platform } from 'ionic-angular'; +import { Config, Platform, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { Network } from '@ionic-native/network'; import { CoreAppProvider } from '../app'; @@ -191,8 +191,9 @@ export class CoreIframeUtilsProvider { * @param {any} element Element to treat (iframe, embed, ...). * @param {Window} contentWindow The window of the element contents. * @param {Document} contentDocument The document of the element contents. + * @param {NavController} [navCtrl] NavController to use if a link can be opened in the app. */ - redefineWindowOpen(element: any, contentWindow: Window, contentDocument: Document): void { + redefineWindowOpen(element: any, contentWindow: Window, contentDocument: Document, navCtrl?: NavController): void { if (contentWindow) { // Intercept window.open. contentWindow.open = (url: string, target: string): Window => { @@ -229,13 +230,18 @@ export class CoreIframeUtilsProvider { this.domUtils.showErrorModal(error); }); } else { - // It's an external link, we will open with browser. Check if we need to auto-login. - if (!this.sitesProvider.isLoggedIn()) { - // Not logged in, cannot auto-login. - this.utils.openInBrowser(url); - } else { - this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url); - } + // It's an external link, check if it can be opened in the app. + this.contentLinksHelper.handleLink(url, undefined, navCtrl, true, true).then((treated) => { + if (!treated) { + // Not opened in the app, open with browser. Check if we need to auto-login + if (!this.sitesProvider.isLoggedIn()) { + // Not logged in, cannot auto-login. + this.utils.openInBrowser(url); + } else { + this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url); + } + } + }); } // We cannot create new Window objects directly, return null which is a valid return value for Window.open(). @@ -248,7 +254,7 @@ export class CoreIframeUtilsProvider { CoreIframeUtilsProvider.FRAME_TAGS.forEach((tag) => { const elements = Array.from(contentDocument.querySelectorAll(tag)); elements.forEach((subElement) => { - this.treatFrame(subElement, true); + this.treatFrame(subElement, true, navCtrl); }); }); } @@ -260,14 +266,15 @@ export class CoreIframeUtilsProvider { * * @param {any} element Element to treat (iframe, embed, ...). * @param {boolean} [isSubframe] Whether it's a frame inside another frame. + * @param {NavController} [navCtrl] NavController to use if a link can be opened in the app. */ - treatFrame(element: any, isSubframe?: boolean): void { + treatFrame(element: any, isSubframe?: boolean, navCtrl?: NavController): void { if (element) { this.checkOnlineFrameInOffline(element, isSubframe); let winAndDoc = this.getContentWindowAndDocument(element); // Redefine window.open in this element and sub frames, it might have been loaded already. - this.redefineWindowOpen(element, winAndDoc.window, winAndDoc.document); + this.redefineWindowOpen(element, winAndDoc.window, winAndDoc.document, navCtrl); // Treat links. this.treatFrameLinks(element, winAndDoc.document); @@ -276,7 +283,7 @@ export class CoreIframeUtilsProvider { // Element loaded, redefine window.open and treat links again. winAndDoc = this.getContentWindowAndDocument(element); - this.redefineWindowOpen(element, winAndDoc.window, winAndDoc.document); + this.redefineWindowOpen(element, winAndDoc.window, winAndDoc.document, navCtrl); this.treatFrameLinks(element, winAndDoc.document); if (winAndDoc.window) { From 7619c63f7062cac4fb92436c111c029c5b99e5e6 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 16 Jul 2019 10:54:24 +0200 Subject: [PATCH 049/241] MOBILE-2930 core: Fix compilation error in block component --- src/core/siteplugins/components/block/block.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/siteplugins/components/block/block.ts b/src/core/siteplugins/components/block/block.ts index d1e927add8d..807bceba388 100644 --- a/src/core/siteplugins/components/block/block.ts +++ b/src/core/siteplugins/components/block/block.ts @@ -27,7 +27,7 @@ import { CoreBlockDelegate } from '@core/block/providers/delegate'; }) export class CoreSitePluginsBlockComponent extends CoreBlockBaseComponent implements OnChanges { @Input() block: any; - @Input() contextLevel: number; + @Input() contextLevel: string; @Input() instanceId: number; @ViewChild(CoreSitePluginsPluginContentComponent) content: CoreSitePluginsPluginContentComponent; From 3e690ad65de61627690e91f164259914d2f1bf7d Mon Sep 17 00:00:00 2001 From: Mark Johnson Date: Mon, 15 Jul 2019 10:50:06 +0100 Subject: [PATCH 050/241] MOBILE-3100 accessibility: Add font size options in general settings --- src/assets/lang/en.json | 2 + src/config.json | 5 +++ src/core/constants.ts | 1 + src/core/settings/lang/en.json | 4 +- src/core/settings/pages/general/general.html | 10 +++++ src/core/settings/pages/general/general.ts | 40 +++++++++++++++++++- src/providers/utils/dom.ts | 5 +++ 7 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index d87fa7cc4a3..4fa2cc62b0f 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1707,6 +1707,8 @@ "core.settings.errorsyncsite": "Error synchronising site data. Please check your Internet connection and try again.", "core.settings.estimatedfreespace": "Estimated free space", "core.settings.filesystemroot": "File system root", + "core.settings.fontsize": "Text size", + "core.settings.fontsizecharacter": "A", "core.settings.general": "General", "core.settings.language": "Language", "core.settings.license": "Licence", diff --git a/src/config.json b/src/config.json index 97284d65863..0091827b524 100644 --- a/src/config.json +++ b/src/config.json @@ -68,6 +68,11 @@ "password": "moodle" } }, + "font_sizes": [ + 62.5, + 75.89, + 93.75 + ], "customurlscheme": "moodlemobile", "siteurl": "", "sitename": "", diff --git a/src/core/constants.ts b/src/core/constants.ts index ea038eecefd..8884ec530e7 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -35,6 +35,7 @@ export class CoreConstants { static SETTINGS_DEBUG_DISPLAY = 'CoreSettingsDebugDisplay'; static SETTINGS_REPORT_IN_BACKGROUND = 'CoreSettingsReportInBackground'; // @deprecated since 3.5.0 static SETTINGS_SEND_ON_ENTER = 'CoreSettingsSendOnEnter'; + static SETTINGS_FONT_SIZE = 'CoreSettingsFontSize'; // WS constants. static WS_TIMEOUT = 30000; diff --git a/src/core/settings/lang/en.json b/src/core/settings/lang/en.json index d2b37188734..b4eab9bced4 100644 --- a/src/core/settings/lang/en.json +++ b/src/core/settings/lang/en.json @@ -28,6 +28,8 @@ "errorsyncsite": "Error synchronising site data. Please check your Internet connection and try again.", "estimatedfreespace": "Estimated free space", "filesystemroot": "File system root", + "fontsize": "Text size", + "fontsizecharacter": "A", "general": "General", "language": "Language", "license": "Licence", @@ -54,4 +56,4 @@ "versioncode": "Version code", "versionname": "Version name", "wificonnection": "Wi-Fi connection" -} \ No newline at end of file +} diff --git a/src/core/settings/pages/general/general.html b/src/core/settings/pages/general/general.html index 2358295dedd..e6b017cc81a 100644 --- a/src/core/settings/pages/general/general.html +++ b/src/core/settings/pages/general/general.html @@ -10,6 +10,16 @@ {{ languages[code] }}
+ +

{{ 'core.settings.fontsize' | translate }}

+ + + {{ 'core.settings.fontsizecharacter' | translate }} + + +

{{ 'core.settings.enablerichtexteditor' | translate }}

diff --git a/src/core/settings/pages/general/general.ts b/src/core/settings/pages/general/general.ts index e6fbd925647..feb6a98550c 100644 --- a/src/core/settings/pages/general/general.ts +++ b/src/core/settings/pages/general/general.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, } from '@angular/core'; -import { IonicPage } from 'ionic-angular'; +import { Component, ViewChild } from '@angular/core'; +import { IonicPage, Segment } from 'ionic-angular'; import { CoreAppProvider } from '@providers/app'; import { CoreConstants } from '@core/constants'; import { CoreConfigProvider } from '@providers/config'; @@ -37,6 +37,8 @@ export class CoreSettingsGeneralPage { languages = {}; languageCodes = []; selectedLanguage: string; + fontSizes = []; + selectedFontSize: string; rteSupported: boolean; richTextEditor: boolean; debugDisplay: boolean; @@ -52,6 +54,24 @@ export class CoreSettingsGeneralPage { this.selectedLanguage = currentLanguage; }); + this.configProvider.get(CoreConstants.SETTINGS_FONT_SIZE, CoreConfigConstants.font_sizes[0]).then((fontSize) => { + this.selectedFontSize = fontSize; + this.fontSizes = CoreConfigConstants.font_sizes.map((size) => { + return { + size: size, + // Absolute pixel size based on 1.4rem body text when this size is selected. + style: Math.round(size * 16 * 1.4 / 100), + selected: size === this.selectedFontSize + }; + }); + // Workaround for segment control bug https://github.com/ionic-team/ionic/issues/6923, fixed in Ionic 4 only. + setTimeout(() => { + if (this.segment) { + this.segment.ngAfterContentInit(); + } + }); + }); + this.rteSupported = this.domUtils.isRichTextEditorSupported(); if (this.rteSupported) { this.configProvider.get(CoreConstants.SETTINGS_RICH_TEXT_EDITOR, true).then((richTextEditorEnabled) => { @@ -64,6 +84,9 @@ export class CoreSettingsGeneralPage { }); } + @ViewChild(Segment) + private segment: Segment; + /** * Called when a new language is selected. */ @@ -73,6 +96,19 @@ export class CoreSettingsGeneralPage { }); } + /** + * Called when a new font size is selected. + */ + fontSizeChanged(): void { + this.fontSizes = this.fontSizes.map((fontSize) => { + fontSize.selected = fontSize.size === this.selectedFontSize; + + return fontSize; + }); + document.documentElement.style.fontSize = this.selectedFontSize + '%'; + this.configProvider.set(CoreConstants.SETTINGS_FONT_SIZE, this.selectedFontSize); + } + /** * Called when the rich text editor is enabled or disabled. */ diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index 4f4fdab17ca..1483a688644 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -22,6 +22,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreTextUtilsProvider } from './text'; import { CoreAppProvider } from '../app'; import { CoreConfigProvider } from '../config'; +import { CoreConfigConstants } from '../../configconstants'; import { CoreUrlUtilsProvider } from './url'; import { CoreFileProvider } from '@providers/file'; import { CoreConstants } from '@core/constants'; @@ -74,6 +75,10 @@ export class CoreDomUtilsProvider { configProvider.get(CoreConstants.SETTINGS_DEBUG_DISPLAY, false).then((debugDisplay) => { this.debugDisplay = !!debugDisplay; }); + // Set the font size based on user preference. + configProvider.get(CoreConstants.SETTINGS_FONT_SIZE, CoreConfigConstants.font_sizes[0]).then((fontSize) => { + document.documentElement.style.fontSize = fontSize + '%'; + }); } /** From b3e1e29932451967593c67c483e84f15fee55822 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 16 Jul 2019 13:44:49 +0200 Subject: [PATCH 051/241] MOBILE-2930 scorm: Fix no visible SCO to load --- src/addon/mod/scorm/pages/player/player.ts | 41 +++++++++++++--------- src/addon/mod/scorm/providers/helper.ts | 16 ++++++--- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/addon/mod/scorm/pages/player/player.ts b/src/addon/mod/scorm/pages/player/player.ts index 63d148aca83..93024042c75 100644 --- a/src/addon/mod/scorm/pages/player/player.ts +++ b/src/addon/mod/scorm/pages/player/player.ts @@ -270,24 +270,31 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { sco.image = this.scormProvider.getScoStatusIcon(sco, this.scorm.incomplete); }); - // Determine current SCO if we received an ID.. - if (this.initialScoId > 0) { - // SCO set by parameter, get it from TOC. - this.currentSco = this.scormHelper.getScoFromToc(this.toc, this.initialScoId); - } - if (!this.currentSco) { - // No SCO defined. Get the first valid one. - return this.scormHelper.getFirstSco(this.scorm.id, this.attempt, this.toc, this.organizationId, this.offline) - .then((sco) => { - - if (sco) { - this.currentSco = sco; - } else { - // We couldn't find a SCO to load: they're all inactive or without launch URL. - this.errorMessage = 'addon.mod_scorm.errornovalidsco'; - } - }); + if (this.newAttempt) { + // Creating a new attempt, use the first SCO defined by the SCORM. + this.initialScoId = this.scorm.launch; + } + + // Determine current SCO if we received an ID. + if (this.initialScoId > 0) { + // SCO set by parameter, get it from TOC. + this.currentSco = this.scormHelper.getScoFromToc(this.toc, this.initialScoId); + } + + if (!this.currentSco) { + // No SCO defined. Get the first valid one. + return this.scormHelper.getFirstSco(this.scorm.id, this.attempt, this.toc, this.organizationId, this.mode, + this.offline).then((sco) => { + + if (sco) { + this.currentSco = sco; + } else { + // We couldn't find a SCO to load: they're all inactive or without launch URL. + this.errorMessage = 'addon.mod_scorm.errornovalidsco'; + } + }); + } } }).finally(() => { this.loadingToc = false; diff --git a/src/addon/mod/scorm/providers/helper.ts b/src/addon/mod/scorm/providers/helper.ts index 95dd3c31b9d..ec0e1523b92 100644 --- a/src/addon/mod/scorm/providers/helper.ts +++ b/src/addon/mod/scorm/providers/helper.ts @@ -199,12 +199,15 @@ export class AddonModScormHelperProvider { * @param {number} attempt Attempt number. * @param {any[]} [toc] SCORM's TOC. If not provided, it will be calculated. * @param {string} [organization] Organization to use. + * @param {string} [mode] Mode. * @param {boolean} [offline] Whether the attempt is offline. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the first SCO. */ - getFirstSco(scormId: number, attempt: number, toc?: any[], organization?: string, offline?: boolean, siteId?: string) - : Promise { + getFirstSco(scormId: number, attempt: number, toc?: any[], organization?: string, mode?: string, offline?: boolean, + siteId?: string): Promise { + + mode = mode || AddonModScormProvider.MODENORMAL; let promise; if (toc && toc.length) { @@ -215,15 +218,20 @@ export class AddonModScormHelperProvider { } return promise.then((scos) => { + // Search the first valid SCO. for (let i = 0; i < scos.length; i++) { const sco = scos[i]; - // Return the first valid and incomplete SCO. - if (sco.isvisible && sco.prereq && sco.launch && this.scormProvider.isStatusIncomplete(sco.status)) { + if (sco.isvisible && sco.launch && sco.prereq && + (mode != AddonModScormProvider.MODENORMAL || this.scormProvider.isStatusIncomplete(sco.status))) { + // In browse/review mode return the first visible sco. In normal mode, first incomplete sco. return sco; } } + + // No "valid" SCO, load the first one. In web it loads the first child because the toc contains the organization SCO. + return scos[0]; }); } From 01b29a4dc2635252c1bdb10f040c20556a29eb19 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Tue, 16 Jul 2019 15:40:18 +0200 Subject: [PATCH 052/241] MOBILE-3075 grades: Handle /grade/report/index.php links --- src/core/grades/providers/user-link-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/grades/providers/user-link-handler.ts b/src/core/grades/providers/user-link-handler.ts index 738c1c0db98..a6952f917a2 100644 --- a/src/core/grades/providers/user-link-handler.ts +++ b/src/core/grades/providers/user-link-handler.ts @@ -24,7 +24,7 @@ import { CoreGradesHelperProvider } from './helper'; @Injectable() export class CoreGradesUserLinkHandler extends CoreContentLinksHandlerBase { name = 'CoreGradesUserLinkHandler'; - pattern = /\/grade\/report\/user\/index.php/; + pattern = /\/grade\/report(\/user)?\/index.php/; constructor(private gradesProvider: CoreGradesProvider, private gradesHelper: CoreGradesHelperProvider) { super(); From 71cd612878dcd17338de36810c9cf47e46f661d2 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 16 Jul 2019 16:40:41 +0200 Subject: [PATCH 053/241] MOBILE-3073 login: Display debug messages when adding site --- .../login/pages/site-error/site-error.html | 2 +- src/core/login/pages/site/site.ts | 10 +- src/providers/sites.ts | 25 +++-- src/providers/utils/dom.ts | 106 ++++++++++-------- 4 files changed, 87 insertions(+), 56 deletions(-) diff --git a/src/core/login/pages/site-error/site-error.html b/src/core/login/pages/site-error/site-error.html index 110a93a7232..da3c478fef2 100644 --- a/src/core/login/pages/site-error/site-error.html +++ b/src/core/login/pages/site-error/site-error.html @@ -20,7 +20,7 @@

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

{{ 'core.login.contactyouradministratorissue' | translate:{$a: ''} }}

-

+

diff --git a/src/core/login/pages/site/site.ts b/src/core/login/pages/site/site.ts index c4bd86fdad1..8b1fcf60004 100644 --- a/src/core/login/pages/site/site.ts +++ b/src/core/login/pages/site/site.ts @@ -154,10 +154,14 @@ export class CoreLoginSitePage { * Show an error that aims people to solve the issue. * * @param {string} url The URL the user was trying to connect to. - * @param {string} error Error to display. + * @param {any} error Error to display. */ - protected showLoginIssue(url: string, error: string): void { - const modal = this.modalCtrl.create('CoreLoginSiteErrorPage', { siteUrl: url, issue: error }); + protected showLoginIssue(url: string, error: any): void { + const modal = this.modalCtrl.create('CoreLoginSiteErrorPage', { + siteUrl: url, + issue: this.domUtils.getErrorMessage(error) + }); + modal.present(); } } diff --git a/src/providers/sites.ts b/src/providers/sites.ts index b64da98fdd1..b01a412d161 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -363,7 +363,7 @@ export class CoreSitesProvider { return this.checkSiteWithProtocol(siteUrl, protocol).catch((error) => { // Do not continue checking if a critical error happened. if (error.critical) { - return Promise.reject(error.error); + return Promise.reject(error); } // Retry with the other protocol. @@ -371,13 +371,17 @@ export class CoreSitesProvider { return this.checkSiteWithProtocol(siteUrl, protocol).catch((secondError) => { if (secondError.critical) { - return Promise.reject(secondError.error); + return Promise.reject(secondError); } // Site doesn't exist. Return the error message. - return Promise.reject(this.textUtils.getErrorMessageFromError(error) || - this.textUtils.getErrorMessageFromError(secondError) || - this.translate.instant('core.cannotconnect')); + if (this.textUtils.getErrorMessageFromError(error)) { + return Promise.reject(error); + } else if (this.textUtils.getErrorMessageFromError(secondError)) { + return Promise.reject(secondError); + } else { + return this.translate.instant('core.cannotconnect'); + } }); }); } @@ -415,8 +419,11 @@ export class CoreSitesProvider { } // Return the error message. - return Promise.reject(this.textUtils.getErrorMessageFromError(error) || - this.textUtils.getErrorMessageFromError(secondError)); + if (this.textUtils.getErrorMessageFromError(error)) { + return Promise.reject(error); + } else { + return Promise.reject(secondError); + } }); }).then(() => { // Create a temporary site to check if local_mobile is installed. @@ -456,7 +463,9 @@ export class CoreSitesProvider { // Error, check if not supported. if (error.available === 1) { // Service supported but an error happened. Return error. - return Promise.reject({ error: error.error }); + error.critical = true; + + return Promise.reject(error); } return data; diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index 4f4fdab17ca..24374f3cd2a 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -598,6 +598,64 @@ export class CoreDomUtilsProvider { return this.textUtils.decodeHTML(this.translate.instant('core.error')); } + /** + * Get the error message from an error, including debug data if needed. + * + * @param {any} error Message to show. + * @param {boolean} [needsTranslate] Whether the error needs to be translated. + * @return {string} Error message, null if no error should be displayed. + */ + getErrorMessage(error: any, needsTranslate?: boolean): string { + let extraInfo = ''; + + if (typeof error == 'object') { + if (this.debugDisplay) { + // Get the debug info. Escape the HTML so it is displayed as it is in the view. + if (error.debuginfo) { + extraInfo = '

' + this.textUtils.escapeHTML(error.debuginfo); + } + if (error.backtrace) { + extraInfo += '

' + this.textUtils.replaceNewLines(this.textUtils.escapeHTML(error.backtrace), '
'); + } + + // tslint:disable-next-line + console.error(error); + } + + // We received an object instead of a string. Search for common properties. + if (error.coreCanceled) { + // It's a canceled error, don't display an error. + return null; + } + + error = this.textUtils.getErrorMessageFromError(error); + if (!error) { + // No common properties found, just stringify it. + error = JSON.stringify(error); + extraInfo = ''; // No need to add extra info because it's already in the error. + } + + // Try to remove tokens from the contents. + const matches = error.match(/token"?[=|:]"?(\w*)/, ''); + if (matches && matches[1]) { + error = error.replace(new RegExp(matches[1], 'g'), 'secret'); + } + } + + if (error == CoreConstants.DONT_SHOW_ERROR) { + // The error shouldn't be shown, stop. + return null; + } + + let message = this.textUtils.decodeHTML(needsTranslate ? this.translate.instant(error) : error); + + if (extraInfo) { + message += extraInfo; + } + + return message; + } + /** * Retrieve component/directive instance. * Please use this function only if you cannot retrieve the instance using parent/child methods: ViewChild (or similar) @@ -1138,51 +1196,11 @@ export class CoreDomUtilsProvider { * @return {Promise} Promise resolved with the alert modal. */ showErrorModal(error: any, needsTranslate?: boolean, autocloseTime?: number): Promise { - let extraInfo = ''; - - if (typeof error == 'object') { - if (this.debugDisplay) { - // Get the debug info. Escape the HTML so it is displayed as it is in the view. - if (error.debuginfo) { - extraInfo = '

' + this.textUtils.escapeHTML(error.debuginfo); - } - if (error.backtrace) { - extraInfo += '

' + this.textUtils.replaceNewLines(this.textUtils.escapeHTML(error.backtrace), '
'); - } - - // tslint:disable-next-line - console.error(error); - } - - // We received an object instead of a string. Search for common properties. - if (error.coreCanceled) { - // It's a canceled error, don't display an error. - return; - } - - error = this.textUtils.getErrorMessageFromError(error); - if (!error) { - // No common properties found, just stringify it. - error = JSON.stringify(error); - extraInfo = ''; // No need to add extra info because it's already in the error. - } - - // Try to remove tokens from the contents. - const matches = error.match(/token"?[=|:]"?(\w*)/, ''); - if (matches && matches[1]) { - error = error.replace(new RegExp(matches[1], 'g'), 'secret'); - } - } - - if (error == CoreConstants.DONT_SHOW_ERROR) { - // The error shouldn't be shown, stop. - return; - } - - let message = this.textUtils.decodeHTML(needsTranslate ? this.translate.instant(error) : error); + const message = this.getErrorMessage(error, needsTranslate); - if (extraInfo) { - message += extraInfo; + if (message === null) { + // Message doesn't need to be displayed, stop. + return Promise.resolve(null); } return this.showAlert(this.getErrorTitle(message), message, undefined, autocloseTime); From 11f34887d0b05ac29614af56950655fee9f0477c Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 17 Jul 2019 12:00:32 +0200 Subject: [PATCH 054/241] MOBILE-2808 datetime: Do not use am/pm in datetime --- src/addon/calendar/pages/event/event.ts | 6 ++--- .../mod/data/fields/date/component/date.ts | 6 ++--- .../datetime/component/datetime.ts | 6 ++--- src/providers/utils/time.ts | 23 +++++++++++++++++++ 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index 0f7fed557ee..ece9ca7bf4a 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -72,9 +72,9 @@ export class AddonCalendarEventPage { this.defaultTime = defaultTime * 60; }); - // Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them. - this.notificationFormat = this.timeUtils.convertPHPToMoment(this.translate.instant('core.strftimedatetime')) - .replace(/[\[\]]/g, ''); + // Calculate format to use. + this.notificationFormat = this.timeUtils.fixFormatForDatetime(this.timeUtils.convertPHPToMoment( + this.translate.instant('core.strftimedatetime'))); } } diff --git a/src/addon/mod/data/fields/date/component/date.ts b/src/addon/mod/data/fields/date/component/date.ts index 187a6fbba36..ff0ab6dedda 100644 --- a/src/addon/mod/data/fields/date/component/date.ts +++ b/src/addon/mod/data/fields/date/component/date.ts @@ -42,9 +42,9 @@ export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginCompo let val; - // Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them. - this.format = this.timeUtils.convertPHPToMoment(this.translate.instant('core.strftimedate')) - .replace(/[\[\]]/g, ''); + // Calculate format to use. + this.format = this.timeUtils.fixFormatForDatetime(this.timeUtils.convertPHPToMoment( + this.translate.instant('core.strftimedate'))); if (this.mode == 'search') { this.addControl('f_' + this.field.id + '_z'); diff --git a/src/addon/userprofilefield/datetime/component/datetime.ts b/src/addon/userprofilefield/datetime/component/datetime.ts index 1d9e9ea036a..8da732a6459 100644 --- a/src/addon/userprofilefield/datetime/component/datetime.ts +++ b/src/addon/userprofilefield/datetime/component/datetime.ts @@ -47,9 +47,9 @@ export class AddonUserProfileFieldDatetimeComponent implements OnInit { // Check if it's only date or it has time too. const hasTime = this.utils.isTrueOrOne(field.param3); - // Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them. - field.format = this.timeUtils.convertPHPToMoment(this.translate.instant('core.' + - (hasTime ? 'strftimedatetime' : 'strftimedate'))).replace(/[\[\]]/g, ''); + // Calculate format to use. + field.format = this.timeUtils.fixFormatForDatetime(this.timeUtils.convertPHPToMoment( + this.translate.instant('core.' + (hasTime ? 'strftimedatetime' : 'strftimedate')))); // Check min value. if (field.param1) { diff --git a/src/providers/utils/time.ts b/src/providers/utils/time.ts index e0c5633e11c..f7c2de9f6ef 100644 --- a/src/providers/utils/time.ts +++ b/src/providers/utils/time.ts @@ -118,6 +118,29 @@ export class CoreTimeUtilsProvider { return converted; } + /** + * Fix format to use in an ion-datetime. + * + * @param {string} format Format to use. + * @return {string} Fixed format. + */ + fixFormatForDatetime(format: string): string { + if (!format) { + return ''; + } + + // The component ion-datetime doesn't support escaping characters ([]), so we remove them. + let fixed = format.replace(/[\[\]]/g, ''); + + if (fixed.indexOf('A') != -1) { + // Do not use am/pm format because there is a bug in ion-datetime. + fixed = fixed.replace(/ ?A/g, ''); + fixed = fixed.replace(/h/g, 'H'); + } + + return fixed; + } + /** * Returns hours, minutes and seconds in a human readable format * From cfaaefc184626283e05d60129921741eece521f5 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 20 Jun 2019 16:15:05 +0200 Subject: [PATCH 055/241] MOBILE-1927 calendar: Allow creating events in online --- src/addon/calendar/lang/en.json | 13 + .../calendar/pages/edit-event/edit-event.html | 144 +++++++ .../pages/edit-event/edit-event.module.ts | 33 ++ .../calendar/pages/edit-event/edit-event.scss | 35 ++ .../calendar/pages/edit-event/edit-event.ts | 392 ++++++++++++++++++ src/addon/calendar/pages/list/list.html | 7 + src/addon/calendar/pages/list/list.ts | 69 ++- src/addon/calendar/providers/calendar.ts | 169 +++++++- src/addon/calendar/providers/helper.ts | 70 ++++ src/assets/lang/en.json | 21 + src/lang/en.json | 8 + src/providers/utils/utils.ts | 44 +- 12 files changed, 992 insertions(+), 13 deletions(-) create mode 100644 src/addon/calendar/pages/edit-event/edit-event.html create mode 100644 src/addon/calendar/pages/edit-event/edit-event.module.ts create mode 100644 src/addon/calendar/pages/edit-event/edit-event.scss create mode 100644 src/addon/calendar/pages/edit-event/edit-event.ts diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index 6ccb04caaf5..3139bb8a983 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -3,13 +3,26 @@ "calendarevents": "Calendar events", "calendarreminders": "Calendar reminders", "defaultnotificationtime": "Default notification time", + "durationminutes": "Duration in minutes", + "durationnone": "Without duration", + "durationuntil": "Until", + "editevent": "Editing event", "errorloadevent": "Error loading event.", "errorloadevents": "Error loading events.", + "eventduration": "Duration", "eventendtime": "End time", + "eventname": "Event title", "eventstarttime": "Start time", + "eventtype": "Event type", "gotoactivity": "Go to activity", + "invalidtimedurationminutes": "The duration in minutes you have entered is invalid. Please enter the duration in minutes greater than 0 or select no duration.", + "invalidtimedurationuntil": "The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.", + "newevent": "New event", "noevents": "There are no events", + "nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event", "reminders": "Reminders", + "repeatevent": "Repeat this event", + "repeatweeksl": "Repeat weekly, creating altogether", "setnewreminder": "Set a new reminder", "typeclose": "Close event", "typecourse": "Course event", diff --git a/src/addon/calendar/pages/edit-event/edit-event.html b/src/addon/calendar/pages/edit-event/edit-event.html new file mode 100644 index 00000000000..bfddc7633f7 --- /dev/null +++ b/src/addon/calendar/pages/edit-event/edit-event.html @@ -0,0 +1,144 @@ + + + + + + + + + + + +
+ + +

{{ 'addon.calendar.eventname' | translate }}

+ + +
+ + + +

{{ 'core.date' | translate }}

+ + +
+ + + +

{{ 'addon.calendar.eventtype' | translate }}

+ + {{ type.name | translate }} + +
+ + + +

{{ 'core.category' | translate }}

+ + {{ category.name }} + +
+ + + +

{{ 'core.course' | translate }}

+ + {{ course.fullname }} + +
+ + + + + +

{{ 'core.course' | translate }}

+ + {{ course.fullname }} + +
+ + +

{{ 'core.coursenogroups' | translate }}

+
+ + +

{{ 'core.group' | translate }}

+ + {{ group.name }} + +
+ + + + +
+ + + + + {{ 'core.showmore' | translate }} + + {{ 'core.showless' | translate }} + + + + + +

{{ 'core.description' | translate }}

+ +
+ + + +

{{ 'core.location' | translate }}

+ +
+ + +
+

{{ 'addon.calendar.eventduration' | translate }}

+ + {{ 'addon.calendar.durationnone' | translate }} + + + + {{ 'addon.calendar.durationuntil' | translate }} + + + + + + + {{ 'addon.calendar.durationminutes' | translate }} + + + + + +
+ + + +

{{ 'addon.calendar.repeatevent' | translate }}

+ +
+ +

{{ 'addon.calendar.repeatweeksl' | translate }}

+ +
+
+ + + + + + + + + + + +
+
+
diff --git a/src/addon/calendar/pages/edit-event/edit-event.module.ts b/src/addon/calendar/pages/edit-event/edit-event.module.ts new file mode 100644 index 00000000000..c5b20e969b5 --- /dev/null +++ b/src/addon/calendar/pages/edit-event/edit-event.module.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonCalendarEditEventPage } from './edit-event'; + +@NgModule({ + declarations: [ + AddonCalendarEditEventPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(AddonCalendarEditEventPage), + TranslateModule.forChild() + ], +}) +export class AddonCalendarEditEventPageModule {} diff --git a/src/addon/calendar/pages/edit-event/edit-event.scss b/src/addon/calendar/pages/edit-event/edit-event.scss new file mode 100644 index 00000000000..3c43c635ef5 --- /dev/null +++ b/src/addon/calendar/pages/edit-event/edit-event.scss @@ -0,0 +1,35 @@ +ion-app.app-root page-addon-calendar-edit-event { + .addon-calendar-duration-container ion-item:not(.addon-calendar-duration-title) { + &.item-ios { + @include padding-horizontal($item-ios-padding-start * 2, null); + + ion-input { + @include padding-horizontal($datetime-ios-padding-start - $text-input-ios-margin-start, null); + } + } + &.item-md { + @include padding-horizontal($item-md-padding-start * 2, null); + + ion-input { + @include padding-horizontal($datetime-md-padding-start - $text-input-md-margin-start, null); + } + } + &.item-wp { + @include padding-horizontal($item-wp-padding-start * 2, null); + + ion-input { + @include padding-horizontal($datetime-wp-padding-start - $text-input-wp-margin-start, null); + } + } + } + + .addon-calendar-eventtype-container.item-select-disabled { + ion-label, ion-select { + opacity: 1; + } + + .select-icon { + display: none; + } + } +} \ No newline at end of file diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts new file mode 100644 index 00000000000..abb8f8b7521 --- /dev/null +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -0,0 +1,392 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, OnInit, Optional, ViewChild } from '@angular/core'; +import { FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { IonicPage, NavController, NavParams } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreGroupsProvider } from '@providers/groups'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreRichTextEditorComponent } from '@components/rich-text-editor/rich-text-editor.ts'; +import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { CoreSite } from '@classes/site'; + +/** + * Page that displays a form to create/edit an event. + */ +@IonicPage({ segment: 'addon-calendar-edit-event' }) +@Component({ + selector: 'page-addon-calendar-edit-event', + templateUrl: 'edit-event.html', +}) +export class AddonCalendarEditEventPage implements OnInit { + + @ViewChild(CoreRichTextEditorComponent) descriptionEditor: CoreRichTextEditorComponent; + + title: string; + dateFormat: string; + component = AddonCalendarProvider.COMPONENT; + loaded = false; + hasOffline = false; + eventTypes = []; + categories = []; + courses = []; + groups = []; + loadingGroups = false; + courseGroupSet = false; + advanced = false; + errors: any; + + // Form variables. + eventForm: FormGroup; + eventTypeControl: FormControl; + groupControl: FormControl; + descriptionControl: FormControl; + + protected eventId: number; + protected courseId: number; + protected originalData: any; + protected currentSite: CoreSite; + protected types: any; // Object with the supported types. + protected showAll: boolean; + + constructor(navParams: NavParams, + private navCtrl: NavController, + private translate: TranslateService, + private domUtils: CoreDomUtilsProvider, + private timeUtils: CoreTimeUtilsProvider, + private eventsProvider: CoreEventsProvider, + private groupsProvider: CoreGroupsProvider, + sitesProvider: CoreSitesProvider, + private coursesProvider: CoreCoursesProvider, + private utils: CoreUtilsProvider, + private calendarProvider: AddonCalendarProvider, + private calendarHelper: AddonCalendarHelperProvider, + private fb: FormBuilder, + @Optional() private svComponent: CoreSplitViewComponent) { + + this.eventId = navParams.get('eventId'); + this.courseId = navParams.get('courseId'); + this.title = this.eventId ? 'addon.calendar.editevent' : 'addon.calendar.newevent'; + + this.currentSite = sitesProvider.getCurrentSite(); + this.errors = { + required: this.translate.instant('core.required') + }; + + // Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them. + this.dateFormat = this.timeUtils.convertPHPToMoment(this.translate.instant('core.strftimedatetimeshort')) + .replace(/[\[\]]/g, ''); + + // Initialize form variables. + this.eventForm = new FormGroup({}); + this.eventTypeControl = this.fb.control('', Validators.required); + this.groupControl = this.fb.control(''); + this.descriptionControl = this.fb.control(''); + + this.eventForm.addControl('name', this.fb.control('', Validators.required)); + this.eventForm.addControl('timestart', this.fb.control(new Date().toISOString(), Validators.required)); + this.eventForm.addControl('eventtype', this.eventTypeControl); + this.eventForm.addControl('categoryid', this.fb.control('')); + this.eventForm.addControl('courseid', this.fb.control(this.courseId)); + this.eventForm.addControl('groupcourseid', this.fb.control('')); + this.eventForm.addControl('groupid', this.groupControl); + this.eventForm.addControl('description', this.descriptionControl); + this.eventForm.addControl('location', this.fb.control('')); + this.eventForm.addControl('duration', this.fb.control(0)); + this.eventForm.addControl('timedurationuntil', this.fb.control(new Date().toISOString())); + this.eventForm.addControl('timedurationminutes', this.fb.control('')); + this.eventForm.addControl('repeat', this.fb.control(false)); + this.eventForm.addControl('repeats', this.fb.control('1')); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.fetchData().finally(() => { + this.originalData = this.utils.clone(this.eventForm.value); + this.loaded = true; + }); + } + + /** + * Fetch the data needed to render the form. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchData(): Promise { + let accessInfo; + + // Get access info. + return this.calendarProvider.getAccessInformation().then((info) => { + accessInfo = info; + + return this.calendarProvider.getAllowedEventTypes(); + }).then((types) => { + this.types = types; + + const promises = [], + eventTypes = this.calendarHelper.getEventTypeOptions(types); + + if (!eventTypes.length) { + return Promise.reject(this.translate.instant('addon.calendar.nopermissiontoupdatecalendar')); + } + + if (types.category) { + // Get the categories. + promises.push(this.coursesProvider.getCategories(0, true).then((cats) => { + this.categories = cats; + })); + } + + this.showAll = this.utils.isTrueOrOne(this.currentSite.getStoredConfig('calendar_adminseesall')) && + accessInfo.canmanageentries; + + if (types.course || types.groups) { + // Get the courses. + const promise = this.showAll ? this.coursesProvider.getCoursesByField() : this.coursesProvider.getUserCourses(); + + promises.push(promise.then((courses) => { + if (this.showAll) { + // Remove site home from the list of courses. + const siteHomeId = this.currentSite.getSiteHomeId(); + courses = courses.filter((course) => { + return course.id != siteHomeId; + }); + } + + // Sort courses by name. + this.courses = courses.sort((a, b) => { + const compareA = a.fullname.toLowerCase(), + compareB = b.fullname.toLowerCase(); + + return compareA.localeCompare(compareB); + }); + })); + } + + return Promise.all(promises).then(() => { + // Set event types. If course is allowed, select it first. + if (types.course) { + this.eventTypeControl.setValue(AddonCalendarProvider.TYPE_COURSE); + } else { + this.eventTypeControl.setValue(eventTypes[0].value); + } + + this.eventTypes = eventTypes; + }); + + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error getting data.'); + this.originalData = null; // Avoid asking for confirmation. + this.navCtrl.pop(); + }); + } + + /** + * Pull to refresh. + * + * @param {any} refresher Refresher. + */ + refreshData(refresher: any): void { + const promises = [ + this.calendarProvider.invalidateAccessInformation(this.courseId), + this.calendarProvider.invalidateAllowedEventTypes(this.courseId) + ]; + + if (this.types) { + if (this.types.category) { + promises.push(this.coursesProvider.invalidateCategories(0, true)); + } + if (this.types.course || this.types.groups) { + if (this.showAll) { + promises.push(this.coursesProvider.invalidateCoursesByField()); + } else { + promises.push(this.coursesProvider.invalidateUserCourses()); + } + } + } + + Promise.all(promises).finally(() => { + this.fetchData().finally(() => { + refresher.complete(); + }); + }); + } + + /** + * A course was selected, get its groups. + * + * @param {number} courseId Course ID. + */ + groupCourseSelected(courseId: number): void { + if (!courseId) { + return; + } + + const modal = this.domUtils.showModalLoading(); + this.loadingGroups = true; + + this.groupsProvider.getUserGroupsInCourse(courseId).then((groups) => { + this.groups = groups; + this.courseGroupSet = true; + this.groupControl.setValue(''); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error getting data.'); + }).finally(() => { + this.loadingGroups = false; + modal.dismiss(); + }); + } + + /** + * Show or hide advanced form fields. + */ + toggleAdvanced(): void { + this.advanced = !this.advanced; + } + + /** + * Create the event. + */ + submit(): void { + // Validate data. + const formData = this.eventForm.value, + timeStartDate = new Date(formData.timestart), + timeUntilDate = new Date(formData.timedurationuntil), + timeDurationMinutes = parseInt(formData.timedurationminutes || '', 10); + let error; + + if (formData.eventtype == AddonCalendarProvider.TYPE_COURSE && !formData.courseid) { + error = 'core.selectacourse'; + } else if (formData.eventtype == AddonCalendarProvider.TYPE_GROUP && !formData.groupcourseid) { + error = 'core.selectacourse'; + } else if (formData.eventtype == AddonCalendarProvider.TYPE_GROUP && !formData.groupid) { + error = 'core.selectagroup'; + } else if (formData.eventtype == AddonCalendarProvider.TYPE_CATEGORY && !formData.categoryid) { + error = 'core.selectacategory'; + } else if (formData.duration == 1 && timeStartDate.getTime() > timeUntilDate.getTime()) { + error = 'addon.calendar.invalidtimedurationuntil'; + } else if (formData.duration == 2 && (isNaN(timeDurationMinutes) || timeDurationMinutes < 1)) { + error = 'addon.calendar.invalidtimedurationminutes'; + } + + if (error) { + // Show error and stop. + this.domUtils.showErrorModal(this.translate.instant(error)); + + return; + } + + // Format the data to send. + const data: any = { + name: formData.name, + eventtype: formData.eventtype, + timestart: Math.floor(timeStartDate.getTime() / 1000), + description: { + text: formData.description, + format: 1 + }, + location: formData.location, + duration: formData.duration, + repeat: formData.repeat + }; + + if (formData.eventtype == AddonCalendarProvider.TYPE_COURSE) { + data.courseid = formData.courseid; + } else if (formData.eventtype == AddonCalendarProvider.TYPE_GROUP) { + data.groupcourseid = formData.groupcourseid; + data.groupid = formData.groupid; + } else if (formData.eventtype == AddonCalendarProvider.TYPE_CATEGORY) { + data.categoryid = formData.categoryid; + } + + if (formData.duration == 1) { + data.timedurationuntil = Math.floor(timeUntilDate.getTime() / 1000); + } else if (formData.duration == 2) { + data.timedurationminutes = formData.timedurationminutes; + } + + if (formData.repeat) { + data.repeats = formData.repeats; + } + + // Send the data. + const modal = this.domUtils.showModalLoading('core.sending'); + + this.calendarProvider.submitEvent(this.eventId, data).then((event) => { + this.returnToList(event); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error sending data.'); + }).finally(() => { + modal.dismiss(); + }); + } + + /** + * Convenience function to update or return to event list depending on device. + * + * @param {number} [event] Event. + */ + protected returnToList(event?: any): void { + const data: any = { + event: event + }; + this.eventsProvider.trigger(AddonCalendarProvider.NEW_EVENT_EVENT, data, this.currentSite.getId()); + + if (this.svComponent && this.svComponent.isOn()) { + // Empty form. + this.hasOffline = false; + this.eventForm.reset(this.originalData); + this.originalData = this.utils.clone(this.eventForm.value); + } else { + this.originalData = null; // Avoid asking for confirmation. + this.navCtrl.pop(); + } + } + + /** + * Discard an offline saved discussion. + */ + discard(): void { + this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => { + // @todo. + }).catch(() => { + // Cancelled. + }); + } + + /** + * Check if we can leave the page or not. + * + * @return {boolean|Promise} Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): boolean | Promise { + + if (this.calendarHelper.hasEventDataChanged(this.eventForm.value, this.originalData)) { + // Show confirmation if some data has been modified. + return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); + } else { + return Promise.resolve(); + } + } +} diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index e5cdc3eb09b..97ae57d40fe 100644 --- a/src/addon/calendar/pages/list/list.html +++ b/src/addon/calendar/pages/list/list.html @@ -40,5 +40,12 @@

+ + + + + \ No newline at end of file diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index 7722dc1c4e1..237ad2457c3 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -54,6 +54,7 @@ export class AddonCalendarListPage implements OnDestroy { protected obsDefaultTimeChange: any; protected eventId: number; protected preSelectedCourseId: number; + protected newEventObserver: any; courses: any[]; eventsLoaded = false; @@ -65,6 +66,7 @@ export class AddonCalendarListPage implements OnDestroy { filter = { course: this.allCourses }; + canCreate = false; constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams, private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, @@ -74,6 +76,7 @@ export class AddonCalendarListPage implements OnDestroy { this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId(); this.notificationsEnabled = localNotificationsProvider.isAvailable(); + if (this.notificationsEnabled) { // Re-schedule events if default time changes. this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => { @@ -83,6 +86,30 @@ export class AddonCalendarListPage implements OnDestroy { this.eventId = navParams.get('eventId') || false; this.preSelectedCourseId = navParams.get('courseId') || null; + + // Listen for events added. When an event is added, we reload the data. + this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { + if (data && data.event) { + if (this.splitviewCtrl.isOn()) { + // Discussion added, clear details page. + this.splitviewCtrl.emptyDetails(); + } + + this.eventsLoaded = false; + this.refreshEvents(false).finally(() => { + this.eventsLoaded = true; + + // In tablet mode try to open the event. + if (this.splitviewCtrl.isOn()) { + if (data.event.id) { + this.gotoEvent(data.event.id); + } else { + // It's an offline event. + } + } + }); + } + }, sitesProvider.getCurrentSiteId()); } /** @@ -114,8 +141,19 @@ export class AddonCalendarListPage implements OnDestroy { this.daysLoaded = 0; this.emptyEventsTimes = 0; + const promises = []; + + if (this.calendarProvider.canEditEventsInSite()) { + // Site allows creating events. Check if the user has permissions to do so. + promises.push(this.calendarProvider.getAllowedEventTypes().then((types) => { + this.canCreate = Object.keys(types).length > 0; + }).catch(() => { + this.canCreate = false; + })); + } + // Load courses for the popover. - return this.coursesProvider.getUserCourses(false).then((courses) => { + promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { // Add "All courses". courses.unshift(this.allCourses); this.courses = courses; @@ -127,7 +165,9 @@ export class AddonCalendarListPage implements OnDestroy { } return this.fetchEvents(refresh); - }); + })); + + return Promise.all(promises); } /** @@ -308,21 +348,23 @@ export class AddonCalendarListPage implements OnDestroy { /** * Refresh the events. * - * @param {any} refresher Refresher. + * @param {any} [refresher] Refresher. + * @return {Promise} Promise resolved when done. */ - refreshEvents(refresher: any): void { + refreshEvents(refresher?: any): Promise { const promises = []; promises.push(this.calendarProvider.invalidateEventsList()); + promises.push(this.calendarProvider.invalidateAllowedEventTypes()); if (this.categoriesRetrieved) { promises.push(this.coursesProvider.invalidateCategories(0, true)); this.categoriesRetrieved = false; } - Promise.all(promises).finally(() => { - this.fetchData(true).finally(() => { - refresher.complete(); + return Promise.all(promises).finally(() => { + return this.fetchData(true).finally(() => { + refresher && refresher.complete(); }); }); } @@ -384,6 +426,18 @@ export class AddonCalendarListPage implements OnDestroy { }); } + /** + * Open page to create an event. + */ + openCreate(): void { + const params: any = {}; + if (this.filter.course.id != this.allCourses.id) { + params.courseId = this.filter.course.id; + } + + this.splitviewCtrl.push('AddonCalendarEditEventPage', params); + } + /** * Open calendar events settings. */ @@ -406,5 +460,6 @@ export class AddonCalendarListPage implements OnDestroy { */ ngOnDestroy(): void { this.obsDefaultTimeChange && this.obsDefaultTimeChange.off(); + this.newEventObserver && this.newEventObserver.off(); } } diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 40dcd2208c4..23f66437190 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -18,6 +18,7 @@ import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreSite } from '@classes/site'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreGroupsProvider } from '@providers/groups'; import { CoreConstants } from '@core/constants'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; @@ -35,6 +36,12 @@ export class AddonCalendarProvider { static DEFAULT_NOTIFICATION_TIME_CHANGED = 'AddonCalendarDefaultNotificationTimeChangedEvent'; static DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime'; static DEFAULT_NOTIFICATION_TIME = 60; + static NEW_EVENT_EVENT = 'addon_calendar_new_event'; + static TYPE_CATEGORY = 'category'; + static TYPE_COURSE = 'course'; + static TYPE_GROUP = 'group'; + static TYPE_SITE = 'site'; + static TYPE_USER = 'user'; protected ROOT_CACHE_KEY = 'mmaCalendar:'; // Variables for database. @@ -206,11 +213,37 @@ export class AddonCalendarProvider { constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider, private coursesProvider: CoreCoursesProvider, private timeUtils: CoreTimeUtilsProvider, - private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider) { + private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider, + private utils: CoreUtilsProvider) { this.logger = logger.getInstance('AddonCalendarProvider'); this.sitesProvider.registerSiteSchema(this.siteSchema); } + /** + * Check if a certain site allows creating and editing events. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if can create/edit. + */ + canEditEvents(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.canEditEventsInSite(site); + }); + } + + /** + * Check if a certain site allows creating and editing events. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether events can be created and edited. + */ + canEditEventsInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + // The WS to create/edit events requires a fix that was integrated in 3.7.1. + return site.isVersionGreaterEqualThan('3.7.1'); + } + /** * Removes expired events from local DB. * @@ -255,6 +288,39 @@ export class AddonCalendarProvider { }); } + /** + * Get access information for a calendar (either course calendar or site calendar). + * + * @param {number} [courseId] Course ID. If not defined, site calendar. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with object with access information. + * @since 3.7 + */ + getAccessInformation(courseId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params: any = {}, + preSets = { + cacheKey: this.getAccessInformationCacheKey(courseId) + }; + + if (courseId) { + params.courseid = courseId; + } + + return site.read('core_calendar_get_calendar_access_information', params, preSets); + }); + } + + /** + * Get cache key for calendar access information WS calls. + * + * @param {number} [courseId] Course ID. + * @return {string} Cache key. + */ + protected getAccessInformationCacheKey(courseId?: number): string { + return this.ROOT_CACHE_KEY + 'accessInformation:' + (courseId || 0); + } + /** * Get all calendar events from local Db. * @@ -267,6 +333,50 @@ export class AddonCalendarProvider { }); } + /** + * Get the type of events a user can create (either course calendar or site calendar). + * + * @param {number} [courseId] Course ID. If not defined, site calendar. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with an object indicating the types. + * @since 3.7 + */ + getAllowedEventTypes(courseId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params: any = {}, + preSets = { + cacheKey: this.getAllowedEventTypesCacheKey(courseId) + }; + + if (courseId) { + params.courseid = courseId; + } + + return site.read('core_calendar_get_allowed_event_types', params, preSets).then((response) => { + // Convert the array to an object. + const result = {}; + + if (response.allowedeventtypes) { + response.allowedeventtypes.map((type) => { + result[type] = true; + }); + } + + return result; + }); + }); + } + + /** + * Get cache key for calendar allowed event types WS calls. + * + * @param {number} [courseId] Course ID. + * @return {string} Cache key. + */ + protected getAllowedEventTypesCacheKey(courseId?: number): string { + return this.ROOT_CACHE_KEY + 'allowedEventTypes:' + (courseId || 0); + } + /** * Get the configured default notification time. * @@ -504,6 +614,32 @@ export class AddonCalendarProvider { return this.getEventsListPrefixCacheKey() + daysToStart + ':' + daysInterval; } + /** + * Invalidates access information. + * + * @param {number} [courseId] Course ID. If not defined, site calendar. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAccessInformation(courseId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getAccessInformationCacheKey(courseId)); + }); + } + + /** + * Invalidates allowed event types. + * + * @param {number} [courseId] Course ID. If not defined, site calendar. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAllowedEventTypes(courseId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getAllowedEventTypesCacheKey(courseId)); + }); + } + /** * Invalidates events list and all the single events and related info. * @@ -780,4 +916,35 @@ export class AddonCalendarProvider { })); }); } + + /** + * Submit an event, either to create it or to edit it. + * + * @param {number} eventId ID of the event. If undefined/null, create a new event. + * @param {any} formData Form data. + * @param {string} [siteId] Site ID. If not provided, current site. + * @return {Promise} Promise resolved when done. + */ + submitEvent(eventId: number, formData: any, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + // Add data that is "hidden" in web. + formData.id = eventId || 0; + formData.userid = site.getUserId(); + formData.visible = 1; + formData.instance = 0; + formData['_qf__core_calendar_local_event_forms_create'] = 1; + + const params = { + formdata: this.utils.objectToGetParams(formData) + }; + + return site.write('core_calendar_submit_create_update_form', params).then((result) => { + if (result.validationerror) { + return Promise.reject(null); + } + + return result.event; + }); + }); + } } diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index a1a85759dcd..818feadf148 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -15,6 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreCourseProvider } from '@core/course/providers/course'; +import { AddonCalendarProvider } from './calendar'; /** * Service that provides some features regarding lists of courses and categories. @@ -47,4 +48,73 @@ export class AddonCalendarHelperProvider { e.moduleIcon = e.icon; } } + + /** + * Get options (name & value) for each allowed event type. + * + * @param {any} eventTypes Result of getAllowedEventTypes. + * @return {{name: string, value: string}[]} Options. + */ + getEventTypeOptions(eventTypes: any): {name: string, value: string}[] { + const options = []; + + if (eventTypes.user) { + options.push({name: 'core.user', value: AddonCalendarProvider.TYPE_USER}); + } + if (eventTypes.group) { + options.push({name: 'core.group', value: AddonCalendarProvider.TYPE_GROUP}); + } + if (eventTypes.course) { + options.push({name: 'core.course', value: AddonCalendarProvider.TYPE_COURSE}); + } + if (eventTypes.category) { + options.push({name: 'core.category', value: AddonCalendarProvider.TYPE_CATEGORY}); + } + if (eventTypes.site) { + options.push({name: 'core.site', value: AddonCalendarProvider.TYPE_SITE}); + } + + return options; + } + + /** + * Check if the data of an event has changed. + * + * @param {any} data Current data. + * @param {any} [original] Original data. + * @return {boolean} True if data has changed, false otherwise. + */ + hasEventDataChanged(data: any, original?: any): boolean { + if (!original) { + // There is no original data, assume it hasn't changed. + return false; + } + + // Check the fields that don't depend on any other. + if (data.name != original.name || data.timestart != original.timestart || data.eventtype != original.eventtype || + data.description != original.description || data.location != original.location || + data.duration != original.duration || data.repeat != original.repeat) { + return true; + } + + // Check data that depends on eventtype. + if ((data.eventtype == AddonCalendarProvider.TYPE_CATEGORY && data.categoryid != original.categoryid) || + (data.eventtype == AddonCalendarProvider.TYPE_COURSE && data.courseid != original.courseid) || + (data.eventtype == AddonCalendarProvider.TYPE_GROUP && data.groupcourseid != original.groupcourseid && + data.groupid != original.groupid)) { + return true; + } + + // Check data that depends on duration. + if ((data.duration == 1 && data.timedurationuntil != original.timedurationuntil) || + (data.duration == 2 && data.timedurationminutes != original.timedurationminutes)) { + return true; + } + + if (data.repeat && data.repeats != original.repeats) { + return true; + } + + return false; + } } diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 0caa8026612..2ed0480e9da 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -87,13 +87,26 @@ "addon.calendar.calendarevents": "Calendar events", "addon.calendar.calendarreminders": "Calendar reminders", "addon.calendar.defaultnotificationtime": "Default notification time", + "addon.calendar.durationminutes": "Duration in minutes", + "addon.calendar.durationnone": "Without duration", + "addon.calendar.durationuntil": "Until", + "addon.calendar.editevent": "Editing event", "addon.calendar.errorloadevent": "Error loading event.", "addon.calendar.errorloadevents": "Error loading events.", + "addon.calendar.eventduration": "Duration", "addon.calendar.eventendtime": "End time", + "addon.calendar.eventname": "Event title", "addon.calendar.eventstarttime": "Start time", + "addon.calendar.eventtype": "Event type", "addon.calendar.gotoactivity": "Go to activity", + "addon.calendar.invalidtimedurationminutes": "The duration in minutes you have entered is invalid. Please enter the duration in minutes greater than 0 or select no duration.", + "addon.calendar.invalidtimedurationuntil": "The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.", + "addon.calendar.newevent": "New event", "addon.calendar.noevents": "There are no events", + "addon.calendar.nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event", "addon.calendar.reminders": "Reminders", + "addon.calendar.repeatevent": "Repeat this event", + "addon.calendar.repeatweeksl": "Repeat weekly, creating altogether", "addon.calendar.setnewreminder": "Set a new reminder", "addon.calendar.typecategory": "Category event", "addon.calendar.typeclose": "Close event", @@ -1328,6 +1341,7 @@ "core.course.warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.", "core.course.warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}", "core.coursedetails": "Course details", + "core.coursenogroups": "This course doesn't have any group.", "core.courses.addtofavourites": "Star this course", "core.courses.allowguests": "This course allows guest users to enter", "core.courses.availablecourses": "Available courses", @@ -1457,6 +1471,7 @@ "core.grades.range": "Range", "core.grades.rank": "Rank", "core.grades.weight": "Weight", + "core.group": "Group", "core.groupsseparate": "Separate groups", "core.groupsvisible": "Visible groups", "core.hasdatatosync": "This {{$a}} has offline data to be synchronised.", @@ -1624,6 +1639,7 @@ "core.nopermissionerror": "Sorry, but you do not currently have permissions to do that", "core.nopermissions": "Sorry, but you do not currently have permissions to do that ({{$a}}).", "core.noresults": "No results", + "core.noselection": "No selection", "core.notapplicable": "n/a", "core.notenrolledprofile": "This profile is not available because this user is not enrolled in this course.", "core.notice": "Notice", @@ -1692,6 +1708,9 @@ "core.sec": "sec", "core.secs": "secs", "core.seemoredetail": "Click here to see more detail", + "core.selectacategory": "Please select a category", + "core.selectacourse": "Select a course", + "core.selectagroup": "Select a group", "core.send": "Send", "core.sending": "Sending", "core.serverconnection": "Error connecting to the server", @@ -1760,6 +1779,7 @@ "core.sharedfiles.sharedfiles": "Shared files", "core.sharedfiles.successstorefile": "File successfully stored. Select the file to upload to your private files or use in an activity.", "core.show": "Show", + "core.showless": "Show less...", "core.showmore": "Show more...", "core.site": "Site", "core.sitehome.sitehome": "Site home", @@ -1808,6 +1828,7 @@ "core.unlimited": "Unlimited", "core.unzipping": "Unzipping", "core.upgraderunning": "Site is being upgraded, please retry later.", + "core.user": "User", "core.user.address": "Address", "core.user.city": "City/town", "core.user.contact": "Contact", diff --git a/src/lang/en.json b/src/lang/en.json index fe910283220..b5c79cb94c4 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -51,6 +51,7 @@ "copiedtoclipboard": "Text copied to clipboard", "course": "Course", "coursedetails": "Course details", + "coursenogroups": "This course doesn't have any group.", "currentdevice": "Current device", "datastoredoffline": "Data stored in the device because it couldn't be sent. It will be sent automatically later.", "date": "Date", @@ -104,6 +105,7 @@ "forcepasswordchangenotice": "You must change your password to proceed.", "fulllistofcourses": "All courses", "fullnameandsitename": "{{fullname}} ({{sitename}})", + "group": "Group", "groupsseparate": "Separate groups", "groupsvisible": "Visible groups", "hasdatatosync": "This {{$a}} has offline data to be synchronised.", @@ -174,6 +176,7 @@ "nopermissionerror": "Sorry, but you do not currently have permissions to do that", "nopermissions": "Sorry, but you do not currently have permissions to do that ({{$a}}).", "noresults": "No results", + "noselection": "No selection", "notapplicable": "n/a", "notenrolledprofile": "This profile is not available because this user is not enrolled in this course.", "notice": "Notice", @@ -214,10 +217,14 @@ "sec": "sec", "secs": "secs", "seemoredetail": "Click here to see more detail", + "selectacategory": "Please select a category", + "selectacourse": "Select a course", + "selectagroup": "Select a group", "send": "Send", "sending": "Sending", "serverconnection": "Error connecting to the server", "show": "Show", + "showless": "Show less...", "showmore": "Show more...", "site": "Site", "sitemaintenance": "The site is undergoing maintenance and is currently not available", @@ -264,6 +271,7 @@ "unlimited": "Unlimited", "unzipping": "Unzipping", "upgraderunning": "Site is being upgraded, please retry later.", + "user": "User", "userdeleted": "This user account has been deleted", "userdetails": "User details", "usernotfullysetup": "User not fully set-up", diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index e1595c75ba9..0f9f71b3b3c 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -376,13 +376,15 @@ export class CoreUtilsProvider { } /** - * Flatten an object, moving subobjects' properties to the first level using dot notation. E.g.: - * {a: {b: 1, c: 2}, d: 3} -> {'a.b': 1, 'a.c': 2, d: 3} + * Flatten an object, moving subobjects' properties to the first level. + * It supports 2 notations: dot notation and square brackets. + * E.g.: {a: {b: 1, c: 2}, d: 3} -> {'a.b': 1, 'a.c': 2, d: 3} * * @param {object} obj Object to flatten. - * @return {object} Flatten object. + * @param {boolean} [useDotNotation] Whether to use dot notation '.' or square brackets '['. + * @return {object} Flattened object. */ - flattenObject(obj: object): object { + flattenObject(obj: object, useDotNotation?: boolean): object { const toReturn = {}; for (const name in obj) { @@ -398,7 +400,8 @@ export class CoreUtilsProvider { continue; } - toReturn[name + '.' + subName] = flatObject[subName]; + const newName = useDotNotation ? name + '.' + subName : name + '[' + subName + ']'; + toReturn[newName] = flatObject[subName]; } } else { toReturn[name] = value; @@ -1051,6 +1054,37 @@ export class CoreUtilsProvider { return mapped; } + /** + * Convert an object to a format of GET param. E.g.: {a: 1, b: 2} -> a=1&b=2 + * + * @param {any} object Object to convert. + * @param {boolean} [removeEmpty=true] Whether to remove params whose value is empty/null/undefined. + * @return {string} GET params. + */ + objectToGetParams(object: any, removeEmpty: boolean = true): string { + // First of all, flatten the object so all properties are in the first level. + const flattened = this.flattenObject(object); + let result = '', + joinChar = ''; + + for (const name in flattened) { + let value = flattened[name]; + + if (removeEmpty && (value === null || typeof value == 'undefined' || value === '')) { + continue; + } + + if (typeof value == 'boolean') { + value = value ? 1 : 0; + } + + result += joinChar + name + '=' + value; + joinChar = '&'; + } + + return result; + } + /** * Add a prefix to all the keys in an object. * From 5a9a7b1a1109a4590606b4965124a119c91afc32 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 21 Jun 2019 08:13:22 +0200 Subject: [PATCH 056/241] MOBILE-1927 calendar: Display group name in event --- src/addon/calendar/pages/event/event.html | 4 ++++ src/addon/calendar/pages/event/event.ts | 20 +++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/addon/calendar/pages/event/event.html b/src/addon/calendar/pages/event/event.html index 3c40c931aff..d823697c6e4 100644 --- a/src/addon/calendar/pages/event/event.html +++ b/src/addon/calendar/pages/event/event.html @@ -26,6 +26,10 @@

{{ 'addon.calendar.eventendtime' | translate}}

{{ 'core.course' | translate}}

+ +

{{ 'core.group' | translate}}

+

{{ groupName }}

+

{{ 'core.category' | translate}}

diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index ece9ca7bf4a..77d4bce092c 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -24,6 +24,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreGroupsProvider } from '@providers/groups'; /** * Page that displays a single calendar event. @@ -46,6 +47,7 @@ export class AddonCalendarEventPage { event: any = {}; title: string; courseName: string; + groupName: string; courseUrl = ''; notificationsEnabled = false; moduleUrl = ''; @@ -58,7 +60,8 @@ export class AddonCalendarEventPage { private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private calendarHelper: AddonCalendarHelperProvider, private sitesProvider: CoreSitesProvider, localNotificationsProvider: CoreLocalNotificationsProvider, private courseProvider: CoreCourseProvider, - private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider) { + private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider, + private groupsProvider: CoreGroupsProvider) { this.eventId = navParams.get('id'); this.notificationsEnabled = localNotificationsProvider.isAvailable(); @@ -165,6 +168,21 @@ export class AddonCalendarEventPage { })); } + // If it's a group event, get the name of the group. + const courseId = canGetById && event.course ? event.course.id : event.courseid; + if (courseId && event.groupid) { + promises.push(this.groupsProvider.getUserGroupsInCourse(event.courseid).then((groups) => { + const group = groups.find((group) => { + return group.id == event.groupid; + }); + + this.groupName = group ? group.name : ''; + }).catch(() => { + // Error getting groups, just don't show the group name. + this.groupName = ''; + })); + } + if (canGetById && event.iscategoryevent) { this.categoryPath = event.category.nestedname; } From 7ccfade21e2c0c118b56928f1213a4158030b8a3 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 21 Jun 2019 09:17:46 +0200 Subject: [PATCH 057/241] MOBILE-1927 calendar: Allow creating events in offline --- src/addon/calendar/calendar.module.ts | 3 + .../calendar/pages/edit-event/edit-event.html | 4 +- .../calendar/pages/edit-event/edit-event.ts | 75 ++++-- src/addon/calendar/pages/list/list.html | 21 +- src/addon/calendar/pages/list/list.ts | 71 ++++-- .../calendar/providers/calendar-offline.ts | 215 ++++++++++++++++++ src/addon/calendar/providers/calendar.ts | 56 ++++- src/addon/calendar/providers/helper.ts | 15 ++ .../mod/data/fields/date/component/date.ts | 4 +- .../mod/data/fields/date/providers/handler.ts | 5 +- src/assets/lang/en.json | 1 + src/lang/en.json | 1 + src/providers/utils/time.ts | 12 + 13 files changed, 438 insertions(+), 45 deletions(-) create mode 100644 src/addon/calendar/providers/calendar-offline.ts diff --git a/src/addon/calendar/calendar.module.ts b/src/addon/calendar/calendar.module.ts index 3146e6e690f..c8131878a5f 100644 --- a/src/addon/calendar/calendar.module.ts +++ b/src/addon/calendar/calendar.module.ts @@ -14,6 +14,7 @@ import { NgModule } from '@angular/core'; import { AddonCalendarProvider } from './providers/calendar'; +import { AddonCalendarOfflineProvider } from './providers/calendar-offline'; import { AddonCalendarHelperProvider } from './providers/helper'; import { AddonCalendarMainMenuHandler } from './providers/mainmenu-handler'; import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; @@ -25,6 +26,7 @@ import { CoreUpdateManagerProvider } from '@providers/update-manager'; // List of providers (without handlers). export const ADDON_CALENDAR_PROVIDERS: any[] = [ AddonCalendarProvider, + AddonCalendarOfflineProvider, AddonCalendarHelperProvider ]; @@ -35,6 +37,7 @@ export const ADDON_CALENDAR_PROVIDERS: any[] = [ ], providers: [ AddonCalendarProvider, + AddonCalendarOfflineProvider, AddonCalendarHelperProvider, AddonCalendarMainMenuHandler ] diff --git a/src/addon/calendar/pages/edit-event/edit-event.html b/src/addon/calendar/pages/edit-event/edit-event.html index bfddc7633f7..0cf40153653 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.html +++ b/src/addon/calendar/pages/edit-event/edit-event.html @@ -44,7 +44,7 @@

{{ 'core.course' | translate }}

- {{ course.fullname }} +
@@ -54,7 +54,7 @@

{{ 'core.course' | translate }}

- {{ course.fullname }} +
diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index abb8f8b7521..d6a6ec48845 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -26,6 +26,7 @@ import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreRichTextEditorComponent } from '@components/rich-text-editor/rich-text-editor.ts'; import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { CoreSite } from '@classes/site'; @@ -79,6 +80,7 @@ export class AddonCalendarEditEventPage implements OnInit { private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, private calendarProvider: AddonCalendarProvider, + private calendarOffline: AddonCalendarOfflineProvider, private calendarHelper: AddonCalendarHelperProvider, private fb: FormBuilder, @Optional() private svComponent: CoreSplitViewComponent) { @@ -102,8 +104,10 @@ export class AddonCalendarEditEventPage implements OnInit { this.groupControl = this.fb.control(''); this.descriptionControl = this.fb.control(''); + const currentDate = this.timeUtils.toDatetimeFormat(); + this.eventForm.addControl('name', this.fb.control('', Validators.required)); - this.eventForm.addControl('timestart', this.fb.control(new Date().toISOString(), Validators.required)); + this.eventForm.addControl('timestart', this.fb.control(currentDate, Validators.required)); this.eventForm.addControl('eventtype', this.eventTypeControl); this.eventForm.addControl('categoryid', this.fb.control('')); this.eventForm.addControl('courseid', this.fb.control(this.courseId)); @@ -112,7 +116,7 @@ export class AddonCalendarEditEventPage implements OnInit { this.eventForm.addControl('description', this.descriptionControl); this.eventForm.addControl('location', this.fb.control('')); this.eventForm.addControl('duration', this.fb.control(0)); - this.eventForm.addControl('timedurationuntil', this.fb.control(new Date().toISOString())); + this.eventForm.addControl('timedurationuntil', this.fb.control(currentDate)); this.eventForm.addControl('timedurationminutes', this.fb.control('')); this.eventForm.addControl('repeat', this.fb.control(false)); this.eventForm.addControl('repeats', this.fb.control('1')); @@ -131,9 +135,10 @@ export class AddonCalendarEditEventPage implements OnInit { /** * Fetch the data needed to render the form. * + * @param {boolean} [refresh] Whether it's refreshing data. * @return {Promise} Promise resolved when done. */ - protected fetchData(): Promise { + protected fetchData(refresh?: boolean): Promise { let accessInfo; // Get access info. @@ -151,6 +156,33 @@ export class AddonCalendarEditEventPage implements OnInit { return Promise.reject(this.translate.instant('addon.calendar.nopermissiontoupdatecalendar')); } + if (this.eventId && !refresh) { + // Get the event data if there's any. + promises.push(this.calendarOffline.getEvent(this.eventId).then((event) => { + this.hasOffline = true; + + // Load the data in the form. + this.eventForm.controls.name.setValue(event.name); + this.eventForm.controls.timestart.setValue(this.timeUtils.toDatetimeFormat(event.timestart * 1000)); + this.eventForm.controls.eventtype.setValue(event.eventtype); + this.eventForm.controls.categoryid.setValue(event.categoryid || ''); + this.eventForm.controls.courseid.setValue(event.courseid || ''); + this.eventForm.controls.groupcourseid.setValue(event.groupcourseid || ''); + this.eventForm.controls.groupid.setValue(event.groupid || ''); + this.eventForm.controls.description.setValue(event.description); + this.eventForm.controls.location.setValue(event.location); + this.eventForm.controls.duration.setValue(event.duration); + this.eventForm.controls.timedurationuntil.setValue( + this.timeUtils.toDatetimeFormat((event.timedurationuntil * 1000) || Date.now())); + this.eventForm.controls.timedurationminutes.setValue(event.timedurationminutes || ''); + this.eventForm.controls.repeat.setValue(!!event.repeat); + this.eventForm.controls.repeats.setValue(event.repeats || '1'); + }).catch(() => { + // No offline data. + this.hasOffline = false; + })); + } + if (types.category) { // Get the categories. promises.push(this.coursesProvider.getCategories(0, true).then((cats) => { @@ -185,11 +217,13 @@ export class AddonCalendarEditEventPage implements OnInit { } return Promise.all(promises).then(() => { - // Set event types. If course is allowed, select it first. - if (types.course) { - this.eventTypeControl.setValue(AddonCalendarProvider.TYPE_COURSE); - } else { - this.eventTypeControl.setValue(eventTypes[0].value); + if (!this.eventTypeControl.value) { + // Initialize event type value. If course is allowed, select it first. + if (types.course) { + this.eventTypeControl.setValue(AddonCalendarProvider.TYPE_COURSE); + } else { + this.eventTypeControl.setValue(eventTypes[0].value); + } } this.eventTypes = eventTypes; @@ -227,7 +261,7 @@ export class AddonCalendarEditEventPage implements OnInit { } Promise.all(promises).finally(() => { - this.fetchData().finally(() => { + this.fetchData(true).finally(() => { refresher.complete(); }); }); @@ -333,8 +367,8 @@ export class AddonCalendarEditEventPage implements OnInit { // Send the data. const modal = this.domUtils.showModalLoading('core.sending'); - this.calendarProvider.submitEvent(this.eventId, data).then((event) => { - this.returnToList(event); + this.calendarProvider.submitEvent(this.eventId, data).then((result) => { + this.returnToList(result.event); }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'Error sending data.'); }).finally(() => { @@ -348,10 +382,14 @@ export class AddonCalendarEditEventPage implements OnInit { * @param {number} [event] Event. */ protected returnToList(event?: any): void { - const data: any = { - event: event - }; - this.eventsProvider.trigger(AddonCalendarProvider.NEW_EVENT_EVENT, data, this.currentSite.getId()); + if (event) { + const data: any = { + event: event + }; + this.eventsProvider.trigger(AddonCalendarProvider.NEW_EVENT_EVENT, data, this.currentSite.getId()); + } else { + this.eventsProvider.trigger(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, {}, this.currentSite.getId()); + } if (this.svComponent && this.svComponent.isOn()) { // Empty form. @@ -369,7 +407,12 @@ export class AddonCalendarEditEventPage implements OnInit { */ discard(): void { this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => { - // @todo. + this.calendarOffline.deleteEvent(this.eventId).then(() => { + this.returnToList(); + }).catch(() => { + // Shouldn't happen. + this.domUtils.showErrorModal('Error discarding event.'); + }); }).catch(() => { // Cancelled. }); diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index 97ae57d40fe..50a57e777a9 100644 --- a/src/addon/calendar/pages/list/list.html +++ b/src/addon/calendar/pages/list/list.html @@ -17,9 +17,28 @@ + + + {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }} + + + + + {{ 'core.notsent' | translate }} +
+ +

+

+ {{ event.timestart * 1000 | coreFormatDate: "strftimedatetimeshort" }} + - {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimedatetimeshort" }} +

+
+ + + @@ -43,7 +62,7 @@

- diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index 237ad2457c3..d799f64c5c9 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -16,6 +16,7 @@ import { Component, ViewChild, OnDestroy } from '@angular/core'; import { IonicPage, Content, PopoverController, NavParams, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; @@ -55,10 +56,12 @@ export class AddonCalendarListPage implements OnDestroy { protected eventId: number; protected preSelectedCourseId: number; protected newEventObserver: any; + protected discardedObserver: any; courses: any[]; eventsLoaded = false; events = []; + offlineEvents = []; notificationsEnabled = false; filteredEvents = []; canLoadMore = false; @@ -67,12 +70,14 @@ export class AddonCalendarListPage implements OnDestroy { course: this.allCourses }; canCreate = false; + hasOffline = false; constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams, private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, private calendarHelper: AddonCalendarHelperProvider, sitesProvider: CoreSitesProvider, localNotificationsProvider: CoreLocalNotificationsProvider, private popoverCtrl: PopoverController, - eventsProvider: CoreEventsProvider, private navCtrl: NavController, appProvider: CoreAppProvider) { + eventsProvider: CoreEventsProvider, private navCtrl: NavController, appProvider: CoreAppProvider, + private calendarOffline: AddonCalendarOfflineProvider) { this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId(); this.notificationsEnabled = localNotificationsProvider.isAvailable(); @@ -87,7 +92,7 @@ export class AddonCalendarListPage implements OnDestroy { this.eventId = navParams.get('eventId') || false; this.preSelectedCourseId = navParams.get('courseId') || null; - // Listen for events added. When an event is added, we reload the data. + // Listen for events added. When an event is added, reload the data. this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { if (data && data.event) { if (this.splitviewCtrl.isOn()) { @@ -97,19 +102,25 @@ export class AddonCalendarListPage implements OnDestroy { this.eventsLoaded = false; this.refreshEvents(false).finally(() => { - this.eventsLoaded = true; - - // In tablet mode try to open the event. - if (this.splitviewCtrl.isOn()) { - if (data.event.id) { - this.gotoEvent(data.event.id); - } else { - // It's an offline event. - } + + // In tablet mode try to open the event (only if it's an online event). + if (this.splitviewCtrl.isOn() && data.event.id > 0) { + this.gotoEvent(data.event.id); } }); } }, sitesProvider.getCurrentSiteId()); + + // Listen for new event discarded event. When it does, reload the data. + this.discardedObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => { + if (this.splitviewCtrl.isOn()) { + // Discussion added, clear details page. + this.splitviewCtrl.emptyDetails(); + } + + this.eventsLoaded = false; + this.refreshEvents(false); + }, sitesProvider.getCurrentSiteId()); } /** @@ -126,8 +137,6 @@ export class AddonCalendarListPage implements OnDestroy { // Take first and load it. this.gotoEvent(this.events[0].id); } - }).finally(() => { - this.eventsLoaded = true; }); } @@ -167,7 +176,18 @@ export class AddonCalendarListPage implements OnDestroy { return this.fetchEvents(refresh); })); - return Promise.all(promises); + // Get offline events. + promises.push(this.calendarOffline.getAllEvents().then((events) => { + this.hasOffline = !!events.length; + + // Format data and sort by timestart. + events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + this.offlineEvents = events.sort((a, b) => a.timestart - b.timestart); + })); + + return Promise.all(promises).finally(() => { + this.eventsLoaded = true; + }); } /** @@ -194,6 +214,8 @@ export class AddonCalendarListPage implements OnDestroy { return this.fetchEvents(); } } else { + events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + // Sort the events by timestart, they're ordered by id. events.sort((a, b) => { if (a.timestart == b.timestart) { @@ -203,7 +225,6 @@ export class AddonCalendarListPage implements OnDestroy { return a.timestart - b.timestart; }); - events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); this.getCategories = this.shouldLoadCategories(events); if (refresh) { @@ -427,10 +448,16 @@ export class AddonCalendarListPage implements OnDestroy { } /** - * Open page to create an event. + * Open page to create/edit an event. + * + * @param {number} [eventId] Event ID to edit. */ - openCreate(): void { + openEdit(eventId?: number): void { const params: any = {}; + + if (eventId) { + params.eventId = eventId; + } if (this.filter.course.id != this.allCourses.id) { params.courseId = this.filter.course.id; } @@ -452,7 +479,15 @@ export class AddonCalendarListPage implements OnDestroy { */ gotoEvent(eventId: number): void { this.eventId = eventId; - this.splitviewCtrl.push('AddonCalendarEventPage', { id: eventId }); + + if (eventId < 0) { + // It's an offline event, go to the edit page. + this.openEdit(eventId); + } else { + this.splitviewCtrl.push('AddonCalendarEventPage', { + id: eventId + }); + } } /** diff --git a/src/addon/calendar/providers/calendar-offline.ts b/src/addon/calendar/providers/calendar-offline.ts new file mode 100644 index 00000000000..c720215bc6b --- /dev/null +++ b/src/addon/calendar/providers/calendar-offline.ts @@ -0,0 +1,215 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; +import { AddonCalendarProvider } from './calendar'; + +/** + * Service to handle offline calendar events. + */ +@Injectable() +export class AddonCalendarOfflineProvider { + + // Variables for database. + static EVENTS_TABLE = 'addon_calendar_offline_events'; + + protected siteSchema: CoreSiteSchema = { + name: 'AddonCalendarOfflineProvider', + version: 1, + tables: [ + { + name: AddonCalendarOfflineProvider.EVENTS_TABLE, + columns: [ + { + name: 'id', // Negative for offline entries. + type: 'INTEGER', + }, + { + name: 'name', + type: 'TEXT', + notNull: true + }, + { + name: 'timestart', + type: 'INTEGER', + notNull: true + }, + { + name: 'eventtype', + type: 'TEXT', + notNull: true + }, + { + name: 'categoryid', + type: 'INTEGER', + }, + { + name: 'courseid', + type: 'INTEGER', + }, + { + name: 'groupcourseid', + type: 'INTEGER', + }, + { + name: 'groupid', + type: 'INTEGER', + }, + { + name: 'description', + type: 'TEXT', + }, + { + name: 'location', + type: 'TEXT', + }, + { + name: 'duration', + type: 'INTEGER', + }, + { + name: 'timedurationuntil', + type: 'INTEGER', + }, + { + name: 'timedurationminutes', + type: 'INTEGER', + }, + { + name: 'repeat', + type: 'INTEGER', + }, + { + name: 'repeats', + type: 'TEXT', + }, + { + name: 'userid', + type: 'INTEGER', + }, + { + name: 'timecreated', + type: 'INTEGER', + } + ], + primaryKeys: ['id'] + } + ] + }; + + constructor(private sitesProvider: CoreSitesProvider) { + this.sitesProvider.registerSiteSchema(this.siteSchema); + } + + /** + * Delete an offline event. + * + * @param {number} eventId Event ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + deleteEvent(eventId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions: any = { + id: eventId + }; + + return site.getDb().deleteRecords(AddonCalendarOfflineProvider.EVENTS_TABLE, conditions); + }); + } + + /** + * Get all offline events. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with events. + */ + getAllEvents(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(AddonCalendarOfflineProvider.EVENTS_TABLE); + }); + } + + /** + * Get an offline event. + * + * @param {number} eventId Event ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the event. + */ + getEvent(eventId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions: any = { + id: eventId + }; + + return site.getDb().getRecord(AddonCalendarOfflineProvider.EVENTS_TABLE, conditions); + }); + } + + /** + * Check if there are offline events to send. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if has offline events, false otherwise. + */ + hasEvents(siteId?: string): Promise { + return this.getAllEvents(siteId).then((events) => { + return !!events.length; + }).catch(() => { + // No offline data found, return false. + return false; + }); + } + + /** + * Offline version for adding a new discussion to a forum. + * + * @param {number} eventId Event ID. If it's a new event, set it to undefined/null. + * @param {any} data Event data. + * @param {number} [timeCreated] The time the event was created. If not defined, current time. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the stored event. + */ + saveEvent(eventId: number, data: any, timeCreated?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + timeCreated = timeCreated || Date.now(); + + const event = { + id: eventId || -timeCreated, + name: data.name, + timestart: data.timestart, + eventtype: data.eventtype, + categoryid: data.categoryid || null, + courseid: data.courseid || null, + groupcourseid: data.groupcourseid || null, + groupid: data.groupid || null, + description: data.description && data.description.text, + location: data.location, + duration: data.duration, + timedurationuntil: data.timedurationuntil, + timedurationminutes: data.timedurationminutes, + repeat: data.repeat ? 1 : 0, + repeats: data.repeats, + timecreated: timeCreated, + userid: site.getUserId() + }; + + return site.getDb().insertRecord(AddonCalendarOfflineProvider.EVENTS_TABLE, event).then(() => { + return event; + }); + }); + } +} diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 23f66437190..6e5bc965b56 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -17,6 +17,7 @@ import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreSite } from '@classes/site'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreAppProvider } from '@providers/app'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreGroupsProvider } from '@providers/groups'; @@ -25,6 +26,7 @@ import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreConfigProvider } from '@providers/config'; import { ILocalNotification } from '@ionic-native/local-notifications'; import { SQLiteDB } from '@classes/sqlitedb'; +import { AddonCalendarOfflineProvider } from './calendar-offline'; /** * Service to handle calendar events. @@ -37,6 +39,7 @@ export class AddonCalendarProvider { static DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime'; static DEFAULT_NOTIFICATION_TIME = 60; static NEW_EVENT_EVENT = 'addon_calendar_new_event'; + static NEW_EVENT_DISCARDED_EVENT = 'addon_calendar_new_event_discarded'; static TYPE_CATEGORY = 'category'; static TYPE_COURSE = 'course'; static TYPE_GROUP = 'group'; @@ -214,7 +217,8 @@ export class AddonCalendarProvider { constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider, private coursesProvider: CoreCoursesProvider, private timeUtils: CoreTimeUtilsProvider, private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider, - private utils: CoreUtilsProvider) { + private utils: CoreUtilsProvider, private calendarOffline: AddonCalendarOfflineProvider, + private appProvider: CoreAppProvider) { this.logger = logger.getInstance('AddonCalendarProvider'); this.sitesProvider.registerSiteSchema(this.siteSchema); } @@ -918,14 +922,58 @@ export class AddonCalendarProvider { } /** - * Submit an event, either to create it or to edit it. + * Submit a calendar event. + * + * @param {number} eventId ID of the event. If undefined/null, create a new event. + * @param {any} formData Form data. + * @param {number} [timeCreated] The time the event was created. Only if modifying a new offline event. + * @param {boolean} [forceOffline] True to always save it in offline. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise<{sent: boolean, event: any}>} Promise resolved with the event and a boolean indicating if data was + * sent to server or stored in offline. + */ + submitEvent(eventId: number, formData: any, timeCreated?: number, forceOffline?: boolean, siteId?: string): + Promise<{sent: boolean, event: any}> { + + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Function to store the event to be synchronized later. + const storeOffline = (): Promise<{sent: boolean, event: any}> => { + return this.calendarOffline.saveEvent(eventId, formData, timeCreated, siteId).then((event) => { + return {sent: false, event: event}; + }); + }; + + if (forceOffline || !this.appProvider.isOnline()) { + // App is offline, store the event. + return storeOffline(); + } + + // If the event is already stored, discard it first. + return this.calendarOffline.deleteEvent(eventId, siteId).then(() => { + return this.submitEventOnline(eventId, formData, siteId).then((event) => { + return {sent: true, event: event}; + }).catch((error) => { + if (error && !this.utils.isWebServiceError(error)) { + // Couldn't connect to server, store in offline. + return storeOffline(); + } else { + // The WebService has thrown an error, reject. + return Promise.reject(error); + } + }); + }); + } + + /** + * Submit an event, either to create it or to edit it. It will fail if offline or cannot connect. * * @param {number} eventId ID of the event. If undefined/null, create a new event. * @param {any} formData Form data. * @param {string} [siteId] Site ID. If not provided, current site. * @return {Promise} Promise resolved when done. */ - submitEvent(eventId: number, formData: any, siteId?: string): Promise { + submitEventOnline(eventId: number, formData: any, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { // Add data that is "hidden" in web. formData.id = eventId || 0; @@ -940,7 +988,7 @@ export class AddonCalendarProvider { return site.write('core_calendar_submit_create_update_form', params).then((result) => { if (result.validationerror) { - return Promise.reject(null); + return Promise.reject(this.utils.createFakeWSError('')); } return result.event; diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index 818feadf148..30d4b952184 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -16,6 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreCourseProvider } from '@core/course/providers/course'; import { AddonCalendarProvider } from './calendar'; +import { CoreConstants } from '@core/constants'; /** * Service that provides some features regarding lists of courses and categories. @@ -47,6 +48,20 @@ export class AddonCalendarHelperProvider { e.icon = this.courseProvider.getModuleIconSrc(e.modulename); e.moduleIcon = e.icon; } + + if (e.id < 0) { + // It's an offline event, add some calculated data. + e.format = 1; + e.visible = 1; + + if (e.duration == 1) { + e.timeduration = e.timedurationuntil - e.timestart; + } else if (e.duration == 2) { + e.timeduration = e.timedurationminutes * CoreConstants.SECONDS_MINUTE; + } else { + e.timeduration = 0; + } + } } /** diff --git a/src/addon/mod/data/fields/date/component/date.ts b/src/addon/mod/data/fields/date/component/date.ts index ff0ab6dedda..c846032a85b 100644 --- a/src/addon/mod/data/fields/date/component/date.ts +++ b/src/addon/mod/data/fields/date/component/date.ts @@ -51,10 +51,10 @@ export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginCompo val = this.search['f_' + this.field.id + '_y'] ? new Date(this.search['f_' + this.field.id + '_y'] + '-' + this.search['f_' + this.field.id + '_m'] + '-' + this.search['f_' + this.field.id + '_d']) : new Date(); - this.search['f_' + this.field.id] = val.toISOString(); + this.search['f_' + this.field.id] = this.timeUtils.toDatetimeFormat(val.getTime()); } else { val = this.value && this.value.content ? new Date(parseInt(this.value.content, 10) * 1000) : new Date(); - val = val.toISOString(); + val = this.timeUtils.toDatetimeFormat(val.getTime()); } this.addControl('f_' + this.field.id, val); diff --git a/src/addon/mod/data/fields/date/providers/handler.ts b/src/addon/mod/data/fields/date/providers/handler.ts index f36d166b702..bf9d6507932 100644 --- a/src/addon/mod/data/fields/date/providers/handler.ts +++ b/src/addon/mod/data/fields/date/providers/handler.ts @@ -15,6 +15,7 @@ import { Injector, Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { AddonModDataFieldHandler } from '../../../providers/fields-delegate'; import { AddonModDataFieldDateComponent } from '../component/date'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; /** * Handler for date data field plugin. @@ -24,7 +25,7 @@ export class AddonModDataFieldDateHandler implements AddonModDataFieldHandler { name = 'AddonModDataFieldDateHandler'; type = 'date'; - constructor(private translate: TranslateService) { } + constructor(private translate: TranslateService, private timeUtils: CoreTimeUtilsProvider) { } /** * Return the Component to use to display the plugin data. @@ -129,7 +130,7 @@ export class AddonModDataFieldDateHandler implements AddonModDataFieldHandler { input = inputData[fieldName] && inputData[fieldName].substr(0, 10) || ''; originalFieldData = (originalFieldData && originalFieldData.content && - new Date(originalFieldData.content * 1000).toISOString().substr(0, 10)) || ''; + this.timeUtils.toDatetimeFormat(originalFieldData.content * 1000).substr(0, 10)) || ''; return input != originalFieldData; } diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 2ed0480e9da..14730d64b18 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1389,6 +1389,7 @@ "core.deleteduser": "Deleted user", "core.deleting": "Deleting", "core.description": "Description", + "core.dfdatetimeinput": "YYYY-MM-DDThh:mm:ss.SSS", "core.dfdaymonthyear": "MM-DD-YYYY", "core.dfdayweekmonth": "ddd, D MMM", "core.dffulldate": "dddd, D MMMM YYYY h[:]mm A", diff --git a/src/lang/en.json b/src/lang/en.json index b5c79cb94c4..714b64a156c 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -64,6 +64,7 @@ "deleteduser": "Deleted user", "deleting": "Deleting", "description": "Description", + "dfdatetimeinput": "YYYY-MM-DDThh:mm:ss.SSS", "dfdaymonthyear": "MM-DD-YYYY", "dfdayweekmonth": "ddd, D MMM", "dffulldate": "dddd, D MMMM YYYY h[:]mm A", diff --git a/src/providers/utils/time.ts b/src/providers/utils/time.ts index f7c2de9f6ef..020860908ca 100644 --- a/src/providers/utils/time.ts +++ b/src/providers/utils/time.ts @@ -299,6 +299,18 @@ export class CoreTimeUtilsProvider { return moment(timestamp).format(format); } + /** + * Convert a timestamp to the format to set to a datetime input. + * + * @param {number} [timestamp] Timestamp to convert (in ms). If not provided, current time. + * @return {string} Formatted time. + */ + toDatetimeFormat(timestamp?: number): string { + timestamp = timestamp || Date.now(); + + return this.userDate(timestamp, 'core.dfdatetimeinput', false); + } + /** * Convert a text into user timezone timestamp. * From 98776a9c781edbcdd5477c76c2b97a1fdef07213 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 21 Jun 2019 15:22:05 +0200 Subject: [PATCH 058/241] MOBILE-1927 calendar: Implement events synchronization --- scripts/langindex.json | 21 ++ src/addon/calendar/calendar.module.ts | 15 +- src/addon/calendar/lang/en.json | 1 + .../calendar/pages/edit-event/edit-event.ts | 89 ++++--- src/addon/calendar/pages/list/list.html | 3 +- src/addon/calendar/pages/list/list.ts | 164 +++++++++---- .../calendar/providers/calendar-offline.ts | 1 - src/addon/calendar/providers/calendar-sync.ts | 230 ++++++++++++++++++ src/addon/calendar/providers/helper.ts | 25 +- .../calendar/providers/sync-cron-handler.ts | 48 ++++ src/assets/lang/en.json | 2 +- src/lang/en.json | 1 - src/providers/utils/time.ts | 17 +- 13 files changed, 537 insertions(+), 80 deletions(-) create mode 100644 src/addon/calendar/providers/calendar-sync.ts create mode 100644 src/addon/calendar/providers/sync-cron-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 5b005c6fbec..dc86f5c8932 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -84,16 +84,30 @@ "addon.blog.showonlyyourentries": "local_moodlemobileapp", "addon.blog.siteblogheading": "blog", "addon.calendar.calendar": "calendar", + "addon.calendar.calendarevent": "local_moodlemobileapp", "addon.calendar.calendarevents": "local_moodlemobileapp", "addon.calendar.calendarreminders": "local_moodlemobileapp", "addon.calendar.defaultnotificationtime": "local_moodlemobileapp", + "addon.calendar.durationminutes": "calendar", + "addon.calendar.durationnone": "calendar", + "addon.calendar.durationuntil": "calendar", + "addon.calendar.editevent": "calendar", "addon.calendar.errorloadevent": "local_moodlemobileapp", "addon.calendar.errorloadevents": "local_moodlemobileapp", + "addon.calendar.eventduration": "calendar", "addon.calendar.eventendtime": "calendar", + "addon.calendar.eventname": "calendar", "addon.calendar.eventstarttime": "calendar", + "addon.calendar.eventtype": "calendar", "addon.calendar.gotoactivity": "calendar", + "addon.calendar.invalidtimedurationminutes": "calendar", + "addon.calendar.invalidtimedurationuntil": "calendar", + "addon.calendar.newevent": "calendar", "addon.calendar.noevents": "local_moodlemobileapp", + "addon.calendar.nopermissiontoupdatecalendar": "error", "addon.calendar.reminders": "local_moodlemobileapp", + "addon.calendar.repeatevent": "calendar", + "addon.calendar.repeatweeksl": "calendar", "addon.calendar.setnewreminder": "local_moodlemobileapp", "addon.calendar.typecategory": "calendar", "addon.calendar.typeclose": "calendar", @@ -1328,6 +1342,7 @@ "core.course.warningmanualcompletionmodified": "local_moodlemobileapp", "core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp", "core.coursedetails": "moodle", + "core.coursenogroups": "local_moodlemobileapp", "core.courses.addtofavourites": "block_myoverview", "core.courses.allowguests": "enrol_guest", "core.courses.availablecourses": "moodle", @@ -1457,6 +1472,7 @@ "core.grades.range": "grades", "core.grades.rank": "grades", "core.grades.weight": "grades", + "core.group": "moodle", "core.groupsseparate": "moodle", "core.groupsvisible": "moodle", "core.hasdatatosync": "local_moodlemobileapp", @@ -1692,6 +1708,9 @@ "core.sec": "moodle", "core.secs": "moodle", "core.seemoredetail": "survey", + "core.selectacategory": "moodle", + "core.selectacourse": "moodle", + "core.selectagroup": "moodle", "core.send": "message", "core.sending": "chat", "core.serverconnection": "error", @@ -1760,6 +1779,7 @@ "core.sharedfiles.sharedfiles": "local_moodlemobileapp", "core.sharedfiles.successstorefile": "local_moodlemobileapp", "core.show": "moodle", + "core.showless": "form", "core.showmore": "form", "core.site": "moodle", "core.sitehome.sitehome": "moodle", @@ -1808,6 +1828,7 @@ "core.unlimited": "moodle", "core.unzipping": "local_moodlemobileapp", "core.upgraderunning": "error", + "core.user": "moodle", "core.user.address": "moodle", "core.user.city": "moodle", "core.user.contact": "local_moodlemobileapp", diff --git a/src/addon/calendar/calendar.module.ts b/src/addon/calendar/calendar.module.ts index c8131878a5f..c8f9cef8209 100644 --- a/src/addon/calendar/calendar.module.ts +++ b/src/addon/calendar/calendar.module.ts @@ -16,8 +16,11 @@ import { NgModule } from '@angular/core'; import { AddonCalendarProvider } from './providers/calendar'; import { AddonCalendarOfflineProvider } from './providers/calendar-offline'; import { AddonCalendarHelperProvider } from './providers/helper'; +import { AddonCalendarSyncProvider } from './providers/calendar-sync'; import { AddonCalendarMainMenuHandler } from './providers/mainmenu-handler'; +import { AddonCalendarSyncCronHandler } from './providers/sync-cron-handler'; import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; +import { CoreCronDelegate } from '@providers/cron'; import { CoreInitDelegate } from '@providers/init'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreLoginHelperProvider } from '@core/login/providers/helper'; @@ -27,7 +30,8 @@ import { CoreUpdateManagerProvider } from '@providers/update-manager'; export const ADDON_CALENDAR_PROVIDERS: any[] = [ AddonCalendarProvider, AddonCalendarOfflineProvider, - AddonCalendarHelperProvider + AddonCalendarHelperProvider, + AddonCalendarSyncProvider ]; @NgModule({ @@ -39,14 +43,19 @@ export const ADDON_CALENDAR_PROVIDERS: any[] = [ AddonCalendarProvider, AddonCalendarOfflineProvider, AddonCalendarHelperProvider, - AddonCalendarMainMenuHandler + AddonCalendarSyncProvider, + AddonCalendarMainMenuHandler, + AddonCalendarSyncCronHandler ] }) export class AddonCalendarModule { constructor(mainMenuDelegate: CoreMainMenuDelegate, calendarHandler: AddonCalendarMainMenuHandler, initDelegate: CoreInitDelegate, calendarProvider: AddonCalendarProvider, loginHelper: CoreLoginHelperProvider, - localNotificationsProvider: CoreLocalNotificationsProvider, updateManager: CoreUpdateManagerProvider) { + localNotificationsProvider: CoreLocalNotificationsProvider, updateManager: CoreUpdateManagerProvider, + cronDelegate: CoreCronDelegate, syncHandler: AddonCalendarSyncCronHandler) { + mainMenuDelegate.registerHandler(calendarHandler); + cronDelegate.register(syncHandler); initDelegate.ready().then(() => { calendarProvider.scheduleAllSitesEventsNotifications(); diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index 3139bb8a983..a7a53183650 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -1,5 +1,6 @@ { "calendar": "Calendar", + "calendarevent": "Calendar event", "calendarevents": "Calendar events", "calendarreminders": "Calendar reminders", "defaultnotificationtime": "Default notification time", diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index d6a6ec48845..bf394c20591 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, Optional, ViewChild } from '@angular/core'; +import { Component, OnInit, OnDestroy, Optional, ViewChild } from '@angular/core'; import { FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms'; import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreGroupsProvider } from '@providers/groups'; import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -28,6 +29,7 @@ import { CoreRichTextEditorComponent } from '@components/rich-text-editor/rich-t import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; import { CoreSite } from '@classes/site'; /** @@ -38,7 +40,7 @@ import { CoreSite } from '@classes/site'; selector: 'page-addon-calendar-edit-event', templateUrl: 'edit-event.html', }) -export class AddonCalendarEditEventPage implements OnInit { +export class AddonCalendarEditEventPage implements OnInit, OnDestroy { @ViewChild(CoreRichTextEditorComponent) descriptionEditor: CoreRichTextEditorComponent; @@ -68,6 +70,7 @@ export class AddonCalendarEditEventPage implements OnInit { protected currentSite: CoreSite; protected types: any; // Object with the supported types. protected showAll: boolean; + protected isDestroyed = false; constructor(navParams: NavParams, private navCtrl: NavController, @@ -82,7 +85,9 @@ export class AddonCalendarEditEventPage implements OnInit { private calendarProvider: AddonCalendarProvider, private calendarOffline: AddonCalendarOfflineProvider, private calendarHelper: AddonCalendarHelperProvider, + private calendarSync: AddonCalendarSyncProvider, private fb: FormBuilder, + private syncProvider: CoreSyncProvider, @Optional() private svComponent: CoreSplitViewComponent) { this.eventId = navParams.get('eventId'); @@ -142,10 +147,10 @@ export class AddonCalendarEditEventPage implements OnInit { let accessInfo; // Get access info. - return this.calendarProvider.getAccessInformation().then((info) => { + return this.calendarProvider.getAccessInformation(this.courseId).then((info) => { accessInfo = info; - return this.calendarProvider.getAllowedEventTypes(); + return this.calendarProvider.getAllowedEventTypes(this.courseId); }).then((types) => { this.types = types; @@ -157,29 +162,38 @@ export class AddonCalendarEditEventPage implements OnInit { } if (this.eventId && !refresh) { - // Get the event data if there's any. - promises.push(this.calendarOffline.getEvent(this.eventId).then((event) => { - this.hasOffline = true; - - // Load the data in the form. - this.eventForm.controls.name.setValue(event.name); - this.eventForm.controls.timestart.setValue(this.timeUtils.toDatetimeFormat(event.timestart * 1000)); - this.eventForm.controls.eventtype.setValue(event.eventtype); - this.eventForm.controls.categoryid.setValue(event.categoryid || ''); - this.eventForm.controls.courseid.setValue(event.courseid || ''); - this.eventForm.controls.groupcourseid.setValue(event.groupcourseid || ''); - this.eventForm.controls.groupid.setValue(event.groupid || ''); - this.eventForm.controls.description.setValue(event.description); - this.eventForm.controls.location.setValue(event.location); - this.eventForm.controls.duration.setValue(event.duration); - this.eventForm.controls.timedurationuntil.setValue( - this.timeUtils.toDatetimeFormat((event.timedurationuntil * 1000) || Date.now())); - this.eventForm.controls.timedurationminutes.setValue(event.timedurationminutes || ''); - this.eventForm.controls.repeat.setValue(!!event.repeat); - this.eventForm.controls.repeats.setValue(event.repeats || '1'); - }).catch(() => { - // No offline data. - this.hasOffline = false; + // If editing an event, get offline data. Wait for sync first. + + promises.push(this.calendarSync.waitForSync(AddonCalendarSyncProvider.SYNC_ID).then(() => { + // Do not block if the scope is already destroyed. + if (!this.isDestroyed) { + this.syncProvider.blockOperation(AddonCalendarProvider.COMPONENT, this.eventId); + } + + // Get the event data if there's any. + return this.calendarOffline.getEvent(this.eventId).then((event) => { + this.hasOffline = true; + + // Load the data in the form. + this.eventForm.controls.name.setValue(event.name); + this.eventForm.controls.timestart.setValue(this.timeUtils.toDatetimeFormat(event.timestart * 1000)); + this.eventForm.controls.eventtype.setValue(event.eventtype); + this.eventForm.controls.categoryid.setValue(event.categoryid || ''); + this.eventForm.controls.courseid.setValue(event.courseid || ''); + this.eventForm.controls.groupcourseid.setValue(event.groupcourseid || ''); + this.eventForm.controls.groupid.setValue(event.groupid || ''); + this.eventForm.controls.description.setValue(event.description); + this.eventForm.controls.location.setValue(event.location); + this.eventForm.controls.duration.setValue(event.duration); + this.eventForm.controls.timedurationuntil.setValue( + this.timeUtils.toDatetimeFormat((event.timedurationuntil * 1000) || Date.now())); + this.eventForm.controls.timedurationminutes.setValue(event.timedurationminutes || ''); + this.eventForm.controls.repeat.setValue(!!event.repeat); + this.eventForm.controls.repeats.setValue(event.repeats || '1'); + }).catch(() => { + // No offline data. + this.hasOffline = false; + }); })); } @@ -305,8 +319,8 @@ export class AddonCalendarEditEventPage implements OnInit { submit(): void { // Validate data. const formData = this.eventForm.value, - timeStartDate = new Date(formData.timestart), - timeUntilDate = new Date(formData.timedurationuntil), + timeStartDate = this.timeUtils.datetimeToDate(formData.timestart), + timeUntilDate = this.timeUtils.datetimeToDate(formData.timedurationuntil), timeDurationMinutes = parseInt(formData.timedurationminutes || '', 10); let error; @@ -382,6 +396,9 @@ export class AddonCalendarEditEventPage implements OnInit { * @param {number} [event] Event. */ protected returnToList(event?: any): void { + // Unblock the sync because the view will be destroyed and the sync process could be triggered before ngOnDestroy. + this.unblockSync(); + if (event) { const data: any = { event: event @@ -432,4 +449,18 @@ export class AddonCalendarEditEventPage implements OnInit { return Promise.resolve(); } } + + protected unblockSync(): void { + if (this.eventId) { + this.syncProvider.unblockOperation(AddonCalendarProvider.COMPONENT, this.eventId); + } + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.unblockSync(); + this.isDestroyed = true; + } } diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index 50a57e777a9..0204b9acf13 100644 --- a/src/addon/calendar/pages/list/list.html +++ b/src/addon/calendar/pages/list/list.html @@ -7,13 +7,14 @@ + - + diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index d799f64c5c9..1a57c4c0bcd 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -12,12 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, ViewChild, OnDestroy } from '@angular/core'; +import { Component, ViewChild, OnDestroy, NgZone } from '@angular/core'; import { IonicPage, Content, PopoverController, NavParams, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -28,6 +29,7 @@ import { CoreEventsProvider } from '@providers/events'; import { CoreAppProvider } from '@providers/app'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import * as moment from 'moment'; +import { Network } from '@ionic-native/network'; /** * Page that displays the list of calendar events. @@ -57,6 +59,8 @@ export class AddonCalendarListPage implements OnDestroy { protected preSelectedCourseId: number; protected newEventObserver: any; protected discardedObserver: any; + protected syncObserver: any; + protected onlineObserver: any; courses: any[]; eventsLoaded = false; @@ -71,13 +75,16 @@ export class AddonCalendarListPage implements OnDestroy { }; canCreate = false; hasOffline = false; + isOnline = false; + syncIcon: string; // Sync icon. constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams, private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, - private calendarHelper: AddonCalendarHelperProvider, sitesProvider: CoreSitesProvider, + private calendarHelper: AddonCalendarHelperProvider, private sitesProvider: CoreSitesProvider, zone: NgZone, localNotificationsProvider: CoreLocalNotificationsProvider, private popoverCtrl: PopoverController, - eventsProvider: CoreEventsProvider, private navCtrl: NavController, appProvider: CoreAppProvider, - private calendarOffline: AddonCalendarOfflineProvider) { + private eventsProvider: CoreEventsProvider, private navCtrl: NavController, private appProvider: CoreAppProvider, + private calendarOffline: AddonCalendarOfflineProvider, private calendarSync: AddonCalendarSyncProvider, + network: Network) { this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId(); this.notificationsEnabled = localNotificationsProvider.isAvailable(); @@ -101,7 +108,7 @@ export class AddonCalendarListPage implements OnDestroy { } this.eventsLoaded = false; - this.refreshEvents(false).finally(() => { + this.refreshEvents(true, false).finally(() => { // In tablet mode try to open the event (only if it's an online event). if (this.splitviewCtrl.isOn() && data.event.id > 0) { @@ -119,8 +126,22 @@ export class AddonCalendarListPage implements OnDestroy { } this.eventsLoaded = false; - this.refreshEvents(false); + this.refreshEvents(true, false); }, sitesProvider.getCurrentSiteId()); + + // Refresh data if calendar events are synchronized automatically. + this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => { + this.eventsLoaded = false; + this.refreshEvents(); + }, sitesProvider.getCurrentSiteId()); + + // Refresh online status when changes. + this.onlineObserver = network.onchange().subscribe((online) => { + // Execute the callback in the Angular zone, so change detection doesn't stop working. + zone.run(() => { + this.isOnline = online; + }); + }); } /** @@ -132,7 +153,9 @@ export class AddonCalendarListPage implements OnDestroy { this.gotoEvent(this.eventId); } - this.fetchData().then(() => { + this.syncIcon = 'spinner'; + + this.fetchData(false, true, false).then(() => { if (!this.eventId && this.splitviewCtrl.isOn() && this.events.length > 0) { // Take first and load it. this.gotoEvent(this.events[0].id); @@ -144,49 +167,76 @@ export class AddonCalendarListPage implements OnDestroy { * Fetch all the data required for the view. * * @param {boolean} [refresh] Empty events array first. + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. * @return {Promise} Promise resolved when done. */ - fetchData(refresh: boolean = false): Promise { + fetchData(refresh?: boolean, sync?: boolean, showErrors?: boolean): Promise { this.daysLoaded = 0; this.emptyEventsTimes = 0; + this.isOnline = this.appProvider.isOnline(); - const promises = []; + let promise; - if (this.calendarProvider.canEditEventsInSite()) { - // Site allows creating events. Check if the user has permissions to do so. - promises.push(this.calendarProvider.getAllowedEventTypes().then((types) => { - this.canCreate = Object.keys(types).length > 0; - }).catch(() => { - this.canCreate = false; - })); + if (sync) { + // Try to synchronize offline events. + promise = this.calendarSync.syncEvents().then((result) => { + if (result.warnings && result.warnings.length) { + this.domUtils.showErrorModal(result.warnings[0]); + } + + if (result.updated) { + // Trigger a manual sync event. + this.eventsProvider.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, { + source: 'list' + }, this.sitesProvider.getCurrentSiteId()); + } + }).catch((error) => { + if (showErrors) { + this.domUtils.showErrorModalDefault(error, 'core.errorsync', true); + } + }); + } else { + promise = Promise.resolve(); } - // Load courses for the popover. - promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { - // Add "All courses". - courses.unshift(this.allCourses); - this.courses = courses; + return promise.then(() => { - if (this.preSelectedCourseId) { - this.filter.course = courses.find((course) => { - return course.id == this.preSelectedCourseId; - }); - } + const promises = []; + const courseId = this.filter.course.id != this.allCourses.id ? this.filter.course.id : undefined; - return this.fetchEvents(refresh); - })); + promises.push(this.calendarHelper.canEditEvents(courseId).then((canEdit) => { + this.canCreate = canEdit; + })); - // Get offline events. - promises.push(this.calendarOffline.getAllEvents().then((events) => { - this.hasOffline = !!events.length; + // Load courses for the popover. + promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { + // Add "All courses". + courses.unshift(this.allCourses); + this.courses = courses; - // Format data and sort by timestart. - events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); - this.offlineEvents = events.sort((a, b) => a.timestart - b.timestart); - })); + if (this.preSelectedCourseId) { + this.filter.course = courses.find((course) => { + return course.id == this.preSelectedCourseId; + }); + } - return Promise.all(promises).finally(() => { + return this.fetchEvents(refresh); + })); + + // Get offline events. + promises.push(this.calendarOffline.getAllEvents().then((events) => { + this.hasOffline = !!events.length; + + // Format data and sort by timestart. + events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + this.offlineEvents = events.sort((a, b) => a.timestart - b.timestart); + })); + + return Promise.all(promises); + }).finally(() => { this.eventsLoaded = true; + this.syncIcon = 'sync'; }); } @@ -196,7 +246,7 @@ export class AddonCalendarListPage implements OnDestroy { * @param {boolean} [refresh] Empty events array first. * @return {Promise} Promise resolved when done. */ - fetchEvents(refresh: boolean = false): Promise { + fetchEvents(refresh?: boolean): Promise { this.loadMoreError = false; return this.calendarProvider.getEventsList(this.daysLoaded, AddonCalendarProvider.DAYS_INTERVAL).then((events) => { @@ -367,12 +417,34 @@ export class AddonCalendarListPage implements OnDestroy { } /** - * Refresh the events. + * Refresh the data. * * @param {any} [refresher] Refresher. + * @param {Function} [done] Function to call when done. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + doRefresh(refresher?: any, done?: () => void, showErrors?: boolean): Promise { + if (this.eventsLoaded) { + return this.refreshEvents(true, showErrors).finally(() => { + refresher && refresher.complete(); + done && done(); + }); + } + + return Promise.resolve(); + } + + /** + * Refresh the events. + * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. * @return {Promise} Promise resolved when done. */ - refreshEvents(refresher?: any): Promise { + refreshEvents(sync?: boolean, showErrors?: boolean): Promise { + this.syncIcon = 'spinner'; + const promises = []; promises.push(this.calendarProvider.invalidateEventsList()); @@ -384,9 +456,7 @@ export class AddonCalendarListPage implements OnDestroy { } return Promise.all(promises).finally(() => { - return this.fetchData(true).finally(() => { - refresher && refresher.complete(); - }); + return this.fetchData(true, sync, showErrors); }); } @@ -440,6 +510,13 @@ export class AddonCalendarListPage implements OnDestroy { this.domUtils.scrollToTop(this.content); this.filteredEvents = this.getFilteredEvents(); + + // Course viewed has changed, check if the user can create events for this course calendar. + const courseId = this.filter.course.id != this.allCourses.id ? this.filter.course.id : undefined; + + this.calendarHelper.canEditEvents(courseId).then((canEdit) => { + this.canCreate = canEdit; + }); } }); popover.present({ @@ -496,5 +573,8 @@ export class AddonCalendarListPage implements OnDestroy { ngOnDestroy(): void { this.obsDefaultTimeChange && this.obsDefaultTimeChange.off(); this.newEventObserver && this.newEventObserver.off(); + this.discardedObserver && this.discardedObserver.off(); + this.syncObserver && this.syncObserver.off(); + this.onlineObserver && this.onlineObserver.off(); } } diff --git a/src/addon/calendar/providers/calendar-offline.ts b/src/addon/calendar/providers/calendar-offline.ts index c720215bc6b..833ca7d4be0 100644 --- a/src/addon/calendar/providers/calendar-offline.ts +++ b/src/addon/calendar/providers/calendar-offline.ts @@ -14,7 +14,6 @@ import { Injectable } from '@angular/core'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; -import { AddonCalendarProvider } from './calendar'; /** * Service to handle offline calendar events. diff --git a/src/addon/calendar/providers/calendar-sync.ts b/src/addon/calendar/providers/calendar-sync.ts new file mode 100644 index 00000000000..cb9eb4b636d --- /dev/null +++ b/src/addon/calendar/providers/calendar-sync.ts @@ -0,0 +1,230 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { TranslateService } from '@ngx-translate/core'; +import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreAppProvider } from '@providers/app'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { AddonCalendarProvider } from './calendar'; +import { AddonCalendarOfflineProvider } from './calendar-offline'; + +/** + * Service to sync calendar. + */ +@Injectable() +export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { + + static AUTO_SYNCED = 'addon_calendar_autom_synced'; + static MANUAL_SYNCED = 'addon_calendar_manual_synced'; + static SYNC_ID = 'calendar'; + + protected componentTranslate: string; + + constructor(translate: TranslateService, + appProvider: CoreAppProvider, + courseProvider: CoreCourseProvider, + private eventsProvider: CoreEventsProvider, + loggerProvider: CoreLoggerProvider, + sitesProvider: CoreSitesProvider, + syncProvider: CoreSyncProvider, + textUtils: CoreTextUtilsProvider, + timeUtils: CoreTimeUtilsProvider, + private utils: CoreUtilsProvider, + private calendarProvider: AddonCalendarProvider, + private calendarOffline: AddonCalendarOfflineProvider) { + + super('AddonCalendarSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, + timeUtils); + + this.componentTranslate = this.translate.instant('addon.calendar.calendarevent'); + } + + /** + * Try to synchronize all events in a certain site or in all sites. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @param {boolean} [force] Wether to force sync not depending on last execution. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllEvents(siteId?: string, force?: boolean): Promise { + return this.syncOnSites('all calendars', this.syncAllEventsFunc.bind(this), [force], siteId); + } + + /** + * Sync all events on a site. + * + * @param {string} siteId Site ID to sync. + * @param {boolean} [force] Wether to force sync not depending on last execution. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + protected syncAllEventsFunc(siteId: string, force?: boolean): Promise { + const promise = force ? this.syncEvents(siteId) : this.syncEventsIfNeeded(siteId); + + return promise.then((result) => { + if (result && result.updated) { + // Sync successful, send event. + this.eventsProvider.trigger(AddonCalendarSyncProvider.AUTO_SYNCED, { + warnings: result.warnings, + events: result.events + }, siteId); + } + }); + } + + /** + * Sync a site events only if a certain time has passed since the last time. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the events are synced or if it doesn't need to be synced. + */ + syncEventsIfNeeded(siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.isSyncNeeded(AddonCalendarSyncProvider.SYNC_ID, siteId).then((needed) => { + if (needed) { + return this.syncEvents(siteId); + } + }); + } + + /** + * Synchronize all offline events of a certain site. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + syncEvents(siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (this.isSyncing(AddonCalendarSyncProvider.SYNC_ID, siteId)) { + // There's already a sync ongoing for this site, return the promise. + return this.getOngoingSync(AddonCalendarSyncProvider.SYNC_ID, siteId); + } + + this.logger.debug('Try to sync calendar events for site ' + siteId); + + const result = { + warnings: [], + events: [], + updated: false + }; + let offlineEvents; + + // Get offline events. + const syncPromise = this.calendarOffline.getAllEvents(siteId).catch(() => { + // No offline data found, return empty list. + return []; + }).then((events) => { + offlineEvents = events; + + if (!events.length) { + // Nothing to sync. + return; + } else if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + return Promise.reject(null); + } + + const promises = []; + + events.forEach((event) => { + promises.push(this.syncOfflineEvent(event, result, siteId)); + }); + + return this.utils.allPromises(promises); + }).then(() => { + if (result.updated) { + // Data has been sent to server. Now invalidate the WS calls. + const promises = [ + this.calendarProvider.invalidateEventsList(siteId), + ]; + + offlineEvents.forEach((event) => { + if (event.id > 0) { + // An event was edited, invalidate its data too. + promises.push(this.calendarProvider.invalidateEvent(event.id, siteId)); + } + }); + + return Promise.all(promises).catch(() => { + // Ignore errors. + }); + } + }).then(() => { + // Sync finished, set sync time. + return this.setSyncTime(AddonCalendarSyncProvider.SYNC_ID, siteId).catch(() => { + // Ignore errors. + }); + }).then(() => { + // All done, return the result. + return result; + }); + + return this.addOngoingSync(AddonCalendarSyncProvider.SYNC_ID, syncPromise, siteId); + } + + /** + * Synchronize an offline event. + * + * @param {any} event The event to sync. + * @param {any} result Object where to store the result of the sync. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + protected syncOfflineEvent(event: any, result: any, siteId?: string): Promise { + + // Verify that event isn't blocked. + if (this.syncProvider.isBlocked(AddonCalendarProvider.COMPONENT, event.id, siteId)) { + this.logger.debug('Cannot sync event ' + event.name + ' because it is blocked.'); + + return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate})); + } + + // Try to send the data. + const data = this.utils.clone(event); // Clone the object because it will be modified in the submit function. + + return this.calendarProvider.submitEventOnline(event.id > 0 ? event.id : undefined, data, siteId).then((newEvent) => { + result.updated = true; + result.events.push(newEvent); + + // Event sent, delete the offline data. + return this.calendarOffline.deleteEvent(event.id, siteId); + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that the event cannot be created. Delete it. + result.updated = true; + + return this.calendarOffline.deleteEvent(event.id, siteId).then(() => { + // Event deleted, add a warning. + result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: event.name, + error: this.textUtils.getErrorMessageFromError(error) + })); + }); + } + + // Local error, reject. + return Promise.reject(error); + }); + } +} diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index 30d4b952184..ae857225d39 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -33,10 +33,33 @@ export class AddonCalendarHelperProvider { category: 'fa-cubes' }; - constructor(logger: CoreLoggerProvider, private courseProvider: CoreCourseProvider) { + constructor(logger: CoreLoggerProvider, private courseProvider: CoreCourseProvider, + private calendarProvider: AddonCalendarProvider) { this.logger = logger.getInstance('AddonCalendarHelperProvider'); } + /** + * Check if current user can create/edit events. + * + * @param {number} [courseId] Course ID. If not defined, site calendar. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: whether the user can create events. + */ + canEditEvents(courseId?: number, siteId?: string): Promise { + return this.calendarProvider.canEditEvents(siteId).then((canEdit) => { + if (!canEdit) { + return false; + } + + // Site allows creating events. Check if the user has permissions to do so. + return this.calendarProvider.getAllowedEventTypes(courseId, siteId).then((types) => { + return Object.keys(types).length > 0; + }); + }).catch(() => { + return false; + }); + } + /** * Convenience function to format some event data to be rendered. * diff --git a/src/addon/calendar/providers/sync-cron-handler.ts b/src/addon/calendar/providers/sync-cron-handler.ts new file mode 100644 index 00000000000..6aabcca742f --- /dev/null +++ b/src/addon/calendar/providers/sync-cron-handler.ts @@ -0,0 +1,48 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreCronHandler } from '@providers/cron'; +import { AddonCalendarSyncProvider } from './calendar-sync'; + +/** + * Synchronization cron handler. + */ +@Injectable() +export class AddonCalendarSyncCronHandler implements CoreCronHandler { + name = 'AddonCalendarSyncCronHandler'; + + constructor(private calendarSync: AddonCalendarSyncProvider) {} + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param {string} [siteId] ID of the site affected, undefined for all sites. + * @param {boolean} [force] Wether the execution is forced (manual sync). + * @return {Promise} Promise resolved when done, rejected if failure. + */ + execute(siteId?: string, force?: boolean): Promise { + return this.calendarSync.syncAllEvents(siteId, force); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return this.calendarSync.syncInterval; + } +} diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 14730d64b18..a7884b46f42 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -84,6 +84,7 @@ "addon.blog.showonlyyourentries": "Show only your entries", "addon.blog.siteblogheading": "Site blog", "addon.calendar.calendar": "Calendar", + "addon.calendar.calendarevent": "Calendar event", "addon.calendar.calendarevents": "Calendar events", "addon.calendar.calendarreminders": "Calendar reminders", "addon.calendar.defaultnotificationtime": "Default notification time", @@ -1389,7 +1390,6 @@ "core.deleteduser": "Deleted user", "core.deleting": "Deleting", "core.description": "Description", - "core.dfdatetimeinput": "YYYY-MM-DDThh:mm:ss.SSS", "core.dfdaymonthyear": "MM-DD-YYYY", "core.dfdayweekmonth": "ddd, D MMM", "core.dffulldate": "dddd, D MMMM YYYY h[:]mm A", diff --git a/src/lang/en.json b/src/lang/en.json index 714b64a156c..b5c79cb94c4 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -64,7 +64,6 @@ "deleteduser": "Deleted user", "deleting": "Deleting", "description": "Description", - "dfdatetimeinput": "YYYY-MM-DDThh:mm:ss.SSS", "dfdaymonthyear": "MM-DD-YYYY", "dfdayweekmonth": "ddd, D MMM", "dffulldate": "dddd, D MMMM YYYY h[:]mm A", diff --git a/src/providers/utils/time.ts b/src/providers/utils/time.ts index 020860908ca..6e2e00aa7ce 100644 --- a/src/providers/utils/time.ts +++ b/src/providers/utils/time.ts @@ -308,7 +308,22 @@ export class CoreTimeUtilsProvider { toDatetimeFormat(timestamp?: number): string { timestamp = timestamp || Date.now(); - return this.userDate(timestamp, 'core.dfdatetimeinput', false); + return this.userDate(timestamp, 'YYYY-MM-DDTHH:mm:ss.SSS', false); + } + + /** + * Convert the value of a ion-datetime to a Date. + * + * @param {string} value Value of ion-datetime. + * @return {Date} Date. + */ + datetimeToDate(value: string): Date { + if (typeof value == 'string' && value.slice(-1) == 'Z') { + // The value shoudln't have the timezone because it causes problems, remove it. + value = value.substr(0, value.length - 1); + } + + return new Date(value); } /** From e740fe4205e1767da235f4d4f912ae200d9dc568 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 26 Jun 2019 08:16:12 +0200 Subject: [PATCH 059/241] MOBILE-3087 calendar: Support editing calendar events --- scripts/langindex.json | 1 + .../calendar/pages/edit-event/edit-event.html | 2 +- .../calendar/pages/edit-event/edit-event.ts | 61 ++++-- src/addon/calendar/pages/event/event.html | 16 +- src/addon/calendar/pages/event/event.ts | 197 ++++++++++++++++-- src/addon/calendar/pages/list/list.html | 2 +- src/addon/calendar/pages/list/list.ts | 42 +++- src/addon/calendar/providers/calendar-sync.ts | 12 +- src/addon/calendar/providers/calendar.ts | 17 +- src/assets/lang/en.json | 1 + src/lang/en.json | 1 + src/providers/utils/utils.ts | 4 +- 12 files changed, 292 insertions(+), 64 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index dc86f5c8932..6103e27f7da 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1484,6 +1484,7 @@ "core.image": "local_moodlemobileapp", "core.imageviewer": "local_moodlemobileapp", "core.info": "moodle", + "core.invalidformdata": "error", "core.ios": "local_moodlemobileapp", "core.labelsep": "langconfig", "core.lastaccess": "moodle", diff --git a/src/addon/calendar/pages/edit-event/edit-event.html b/src/addon/calendar/pages/edit-event/edit-event.html index 0cf40153653..96edde1b9cf 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.html +++ b/src/addon/calendar/pages/edit-event/edit-event.html @@ -134,7 +134,7 @@ - + diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index bf394c20591..e800800c2fb 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -162,7 +162,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { } if (this.eventId && !refresh) { - // If editing an event, get offline data. Wait for sync first. + // Editing an event, get the event data. Wait for sync first. promises.push(this.calendarSync.waitForSync(AddonCalendarSyncProvider.SYNC_ID).then(() => { // Do not block if the scope is already destroyed. @@ -170,29 +170,38 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { this.syncProvider.blockOperation(AddonCalendarProvider.COMPONENT, this.eventId); } - // Get the event data if there's any. + // Get the event offline data if there's any. return this.calendarOffline.getEvent(this.eventId).then((event) => { this.hasOffline = true; - // Load the data in the form. - this.eventForm.controls.name.setValue(event.name); - this.eventForm.controls.timestart.setValue(this.timeUtils.toDatetimeFormat(event.timestart * 1000)); - this.eventForm.controls.eventtype.setValue(event.eventtype); - this.eventForm.controls.categoryid.setValue(event.categoryid || ''); - this.eventForm.controls.courseid.setValue(event.courseid || ''); - this.eventForm.controls.groupcourseid.setValue(event.groupcourseid || ''); - this.eventForm.controls.groupid.setValue(event.groupid || ''); - this.eventForm.controls.description.setValue(event.description); - this.eventForm.controls.location.setValue(event.location); - this.eventForm.controls.duration.setValue(event.duration); - this.eventForm.controls.timedurationuntil.setValue( - this.timeUtils.toDatetimeFormat((event.timedurationuntil * 1000) || Date.now())); - this.eventForm.controls.timedurationminutes.setValue(event.timedurationminutes || ''); - this.eventForm.controls.repeat.setValue(!!event.repeat); - this.eventForm.controls.repeats.setValue(event.repeats || '1'); + return event; }).catch(() => { // No offline data. this.hasOffline = false; + + if (this.eventId > 0) { + // It's an online event. get its data from server. + return this.calendarProvider.getEventById(this.eventId); + } + }).then((event) => { + if (event) { + // Load the data in the form. + this.eventForm.controls.name.setValue(event.name); + this.eventForm.controls.timestart.setValue(this.timeUtils.toDatetimeFormat(event.timestart * 1000)); + this.eventForm.controls.eventtype.setValue(event.eventtype); + this.eventForm.controls.categoryid.setValue(event.categoryid || ''); + this.eventForm.controls.courseid.setValue(event.courseid || ''); + this.eventForm.controls.groupcourseid.setValue(event.groupcourseid || ''); + this.eventForm.controls.groupid.setValue(event.groupid || ''); + this.eventForm.controls.description.setValue(event.description); + this.eventForm.controls.location.setValue(event.location); + this.eventForm.controls.duration.setValue(event.duration); + this.eventForm.controls.timedurationuntil.setValue( + this.timeUtils.toDatetimeFormat((event.timedurationuntil * 1000) || Date.now())); + this.eventForm.controls.timedurationminutes.setValue(event.timedurationminutes || ''); + this.eventForm.controls.repeat.setValue(!!event.repeat); + this.eventForm.controls.repeats.setValue(event.repeats || '1'); + } }); })); } @@ -379,7 +388,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { } // Send the data. - const modal = this.domUtils.showModalLoading('core.sending'); + const modal = this.domUtils.showModalLoading('core.sending', true); this.calendarProvider.submitEvent(this.eventId, data).then((result) => { this.returnToList(result.event); @@ -399,13 +408,21 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { // Unblock the sync because the view will be destroyed and the sync process could be triggered before ngOnDestroy. this.unblockSync(); - if (event) { + if (this.eventId > 0) { + // Editing an event. const data: any = { event: event }; - this.eventsProvider.trigger(AddonCalendarProvider.NEW_EVENT_EVENT, data, this.currentSite.getId()); + this.eventsProvider.trigger(AddonCalendarProvider.EDIT_EVENT_EVENT, data, this.currentSite.getId()); } else { - this.eventsProvider.trigger(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, {}, this.currentSite.getId()); + if (event) { + const data: any = { + event: event + }; + this.eventsProvider.trigger(AddonCalendarProvider.NEW_EVENT_EVENT, data, this.currentSite.getId()); + } else { + this.eventsProvider.trigger(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, {}, this.currentSite.getId()); + } } if (this.svComponent && this.svComponent.isOn()) { diff --git a/src/addon/calendar/pages/event/event.html b/src/addon/calendar/pages/event/event.html index d823697c6e4..93e5bd28507 100644 --- a/src/addon/calendar/pages/event/event.html +++ b/src/addon/calendar/pages/event/event.html @@ -1,13 +1,27 @@ + + + + + + + + + - + + + + {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendarevent' | translate} }} + + diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index 77d4bce092c..6fb0a16d11f 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -12,12 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, ViewChild } from '@angular/core'; -import { IonicPage, Content, NavParams } from 'ionic-angular'; +import { Component, ViewChild, Optional, OnDestroy, NgZone } from '@angular/core'; +import { IonicPage, Content, NavParams, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; +import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreAppProvider } from '@providers/app'; +import { CoreEventsProvider } from '@providers/events'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreSitesProvider } from '@providers/sites'; @@ -25,6 +29,8 @@ import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreGroupsProvider } from '@providers/groups'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { Network } from '@ionic-native/network'; /** * Page that displays a single calendar event. @@ -34,11 +40,17 @@ import { CoreGroupsProvider } from '@providers/groups'; selector: 'page-addon-calendar-event', templateUrl: 'event.html', }) -export class AddonCalendarEventPage { +export class AddonCalendarEventPage implements OnDestroy { @ViewChild(Content) content: Content; protected eventId; protected siteHomeId: number; + protected editEventObserver: any; + protected syncObserver: any; + protected manualSyncObserver: any; + protected onlineObserver: any; + protected currentSiteId: string; + eventLoaded: boolean; notificationFormat: string; notificationMin: string; @@ -55,17 +67,31 @@ export class AddonCalendarEventPage { currentTime: number; defaultTime: number; reminders: any[]; + canEdit = false; + hasOffline = false; + isOnline = false; + syncIcon: string; // Sync icon. + isSplitViewOn = false; constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams, private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private calendarHelper: AddonCalendarHelperProvider, private sitesProvider: CoreSitesProvider, localNotificationsProvider: CoreLocalNotificationsProvider, private courseProvider: CoreCourseProvider, private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider, - private groupsProvider: CoreGroupsProvider) { + private groupsProvider: CoreGroupsProvider, @Optional() private svComponent: CoreSplitViewComponent, + private navCtrl: NavController, private eventsProvider: CoreEventsProvider, network: Network, zone: NgZone, + private calendarSync: AddonCalendarSyncProvider, private appProvider: CoreAppProvider, + private calendarOffline: AddonCalendarOfflineProvider) { this.eventId = navParams.get('id'); this.notificationsEnabled = localNotificationsProvider.isAvailable(); this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId(); + this.currentSiteId = sitesProvider.getCurrentSiteId(); + this.isSplitViewOn = this.svComponent && this.svComponent.isOn(); + + // Check if site supports editing. No need to check allowed types, event.canedit already does it. + this.canEdit = this.calendarProvider.canEditEventsInSite(); + if (this.notificationsEnabled) { this.calendarProvider.getEventReminders(this.eventId).then((reminders) => { this.reminders = reminders; @@ -79,34 +105,105 @@ export class AddonCalendarEventPage { this.notificationFormat = this.timeUtils.fixFormatForDatetime(this.timeUtils.convertPHPToMoment( this.translate.instant('core.strftimedatetime'))); } + + // Listen for event edited. If current event is edited, reload the data. + this.editEventObserver = eventsProvider.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => { + if (data && data.event && data.event.id == this.eventId) { + this.eventLoaded = false; + this.refreshEvent(true, false); + } + }, this.currentSiteId); + + // Refresh data if this calendar event is synchronized automatically. + this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, this.checkSyncResult.bind(this, false), + this.currentSiteId); + + // Refresh data if calendar events are synchronized manually but not by this page. + this.manualSyncObserver = eventsProvider.on(AddonCalendarSyncProvider.MANUAL_SYNCED, this.checkSyncResult.bind(this, true), + this.currentSiteId); + + // Refresh online status when changes. + this.onlineObserver = network.onchange().subscribe((online) => { + // Execute the callback in the Angular zone, so change detection doesn't stop working. + zone.run(() => { + this.isOnline = online; + }); + }); } /** * View loaded. */ ionViewDidLoad(): void { - this.fetchEvent().finally(() => { - this.eventLoaded = true; - }); + this.syncIcon = 'spinner'; + + this.fetchEvent(); } /** * Fetches the event and updates the view. * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. * @return {Promise} Promise resolved when done. */ - fetchEvent(): Promise { + fetchEvent(sync?: boolean, showErrors?: boolean): Promise { const currentSite = this.sitesProvider.getCurrentSite(), canGetById = this.calendarProvider.isGetEventByIdAvailable(); let promise; - if (canGetById) { - promise = this.calendarProvider.getEventById(this.eventId); + this.isOnline = this.appProvider.isOnline(); + + if (sync) { + // Try to synchronize offline events. + promise = this.calendarSync.syncEvents().then((result) => { + if (result.warnings && result.warnings.length) { + this.domUtils.showErrorModal(result.warnings[0]); + } + + if (result.updated) { + // Trigger a manual sync event. + result.source = 'event'; + + this.eventsProvider.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId); + } + }).catch((error) => { + if (showErrors) { + this.domUtils.showErrorModalDefault(error, 'core.errorsync', true); + } + }); } else { - promise = this.calendarProvider.getEvent(this.eventId); + promise = Promise.resolve(); } - return promise.then((event) => { + return promise.then(() => { + const promises = []; + + // Get the event data. + if (canGetById) { + promises.push(this.calendarProvider.getEventById(this.eventId)); + } else { + promises.push(this.calendarProvider.getEvent(this.eventId)); + } + + // Get offline data. + promises.push(this.calendarOffline.getEvent(this.eventId).catch(() => { + // No offline data. + })); + + return Promise.all(promises).then((results) => { + if (results[1]) { + // There is offline data, apply it. + this.hasOffline = true; + Object.assign(results[0], results[1]); + } else { + this.hasOffline = false; + } + + return results[0]; + }); + + }).then((event) => { const promises = []; this.calendarHelper.formatEventData(event); @@ -196,6 +293,9 @@ export class AddonCalendarEventPage { return Promise.all(promises); }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevent', true); + }).finally(() => { + this.eventLoaded = true; + this.syncIcon = 'sync'; }); } @@ -247,15 +347,76 @@ export class AddonCalendarEventPage { } /** - * Refresh the event. + * Refresh the data. * - * @param {any} refresher Refresher. + * @param {any} [refresher] Refresher. + * @param {Function} [done] Function to call when done. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. */ - refreshEvent(refresher: any): void { - this.calendarProvider.invalidateEvent(this.eventId).finally(() => { - this.fetchEvent().finally(() => { - refresher.complete(); + doRefresh(refresher?: any, done?: () => void, showErrors?: boolean): Promise { + if (this.eventLoaded) { + return this.refreshEvent(true, showErrors).finally(() => { + refresher && refresher.complete(); + done && done(); }); + } + + return Promise.resolve(); + } + + /** + * Refresh the event. + * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + refreshEvent(sync?: boolean, showErrors?: boolean): Promise { + this.syncIcon = 'spinner'; + + return this.calendarProvider.invalidateEvent(this.eventId).catch(() => { + // Ignore errors. + }).then(() => { + return this.fetchEvent(sync, showErrors); }); } + + /** + * Open the page to edit the event. + */ + openEdit(): void { + // Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav. + const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; + navCtrl.push('AddonCalendarEditEventPage', {eventId: this.eventId}); + } + + /** + * Check the result of an automatic sync or a manual sync not done by this page. + * + * @param {boolean} isManual Whether it's a manual sync. + * @param {any} data Sync result. + */ + protected checkSyncResult(isManual: boolean, data: any): void { + if (data && data.events && (!isManual || data.source != 'event')) { + const event = data.events.find((ev) => { + return ev.id == this.eventId; + }); + + if (event) { + this.eventLoaded = false; + this.refreshEvent(); + } + } + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.editEventObserver && this.editEventObserver.off(); + this.syncObserver && this.syncObserver.off(); + this.manualSyncObserver && this.manualSyncObserver.off(); + this.onlineObserver && this.onlineObserver.unsubscribe(); + } } diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index 0204b9acf13..57563190d00 100644 --- a/src/addon/calendar/pages/list/list.html +++ b/src/addon/calendar/pages/list/list.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index 1a57c4c0bcd..3418f051484 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -59,8 +59,11 @@ export class AddonCalendarListPage implements OnDestroy { protected preSelectedCourseId: number; protected newEventObserver: any; protected discardedObserver: any; + protected editEventObserver: any; protected syncObserver: any; + protected manualSyncObserver: any; protected onlineObserver: any; + protected currentSiteId: string; courses: any[]; eventsLoaded = false; @@ -80,7 +83,7 @@ export class AddonCalendarListPage implements OnDestroy { constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams, private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, - private calendarHelper: AddonCalendarHelperProvider, private sitesProvider: CoreSitesProvider, zone: NgZone, + private calendarHelper: AddonCalendarHelperProvider, sitesProvider: CoreSitesProvider, zone: NgZone, localNotificationsProvider: CoreLocalNotificationsProvider, private popoverCtrl: PopoverController, private eventsProvider: CoreEventsProvider, private navCtrl: NavController, private appProvider: CoreAppProvider, private calendarOffline: AddonCalendarOfflineProvider, private calendarSync: AddonCalendarSyncProvider, @@ -88,12 +91,13 @@ export class AddonCalendarListPage implements OnDestroy { this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId(); this.notificationsEnabled = localNotificationsProvider.isAvailable(); + this.currentSiteId = sitesProvider.getCurrentSiteId(); if (this.notificationsEnabled) { // Re-schedule events if default time changes. this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => { calendarProvider.scheduleEventsNotifications(this.events); - }, sitesProvider.getCurrentSiteId()); + }, this.currentSiteId); } this.eventId = navParams.get('eventId') || false; @@ -116,7 +120,7 @@ export class AddonCalendarListPage implements OnDestroy { } }); } - }, sitesProvider.getCurrentSiteId()); + }, this.currentSiteId); // Listen for new event discarded event. When it does, reload the data. this.discardedObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => { @@ -127,13 +131,29 @@ export class AddonCalendarListPage implements OnDestroy { this.eventsLoaded = false; this.refreshEvents(true, false); - }, sitesProvider.getCurrentSiteId()); + }, this.currentSiteId); + + // Listen for events edited. When an event is edited, reload the data. + this.editEventObserver = eventsProvider.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => { + if (data && data.event) { + this.eventsLoaded = false; + this.refreshEvents(true, false); + } + }, this.currentSiteId); // Refresh data if calendar events are synchronized automatically. this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => { this.eventsLoaded = false; this.refreshEvents(); - }, sitesProvider.getCurrentSiteId()); + }, this.currentSiteId); + + // Refresh data if calendar events are synchronized manually but not by this page. + this.manualSyncObserver = eventsProvider.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => { + if (data && data.source != 'list') { + this.eventsLoaded = false; + this.refreshEvents(); + } + }, this.currentSiteId); // Refresh online status when changes. this.onlineObserver = network.onchange().subscribe((online) => { @@ -187,9 +207,9 @@ export class AddonCalendarListPage implements OnDestroy { if (result.updated) { // Trigger a manual sync event. - this.eventsProvider.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, { - source: 'list' - }, this.sitesProvider.getCurrentSiteId()); + result.source = 'list'; + + this.eventsProvider.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId); } }).catch((error) => { if (showErrors) { @@ -530,6 +550,8 @@ export class AddonCalendarListPage implements OnDestroy { * @param {number} [eventId] Event ID to edit. */ openEdit(eventId?: number): void { + this.eventId = undefined; + const params: any = {}; if (eventId) { @@ -574,7 +596,9 @@ export class AddonCalendarListPage implements OnDestroy { this.obsDefaultTimeChange && this.obsDefaultTimeChange.off(); this.newEventObserver && this.newEventObserver.off(); this.discardedObserver && this.discardedObserver.off(); + this.editEventObserver && this.editEventObserver.off(); this.syncObserver && this.syncObserver.off(); - this.onlineObserver && this.onlineObserver.off(); + this.manualSyncObserver && this.manualSyncObserver.off(); + this.onlineObserver && this.onlineObserver.unsubscribe(); } } diff --git a/src/addon/calendar/providers/calendar-sync.ts b/src/addon/calendar/providers/calendar-sync.ts index cb9eb4b636d..834d1ac34ec 100644 --- a/src/addon/calendar/providers/calendar-sync.ts +++ b/src/addon/calendar/providers/calendar-sync.ts @@ -37,8 +37,6 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { static MANUAL_SYNCED = 'addon_calendar_manual_synced'; static SYNC_ID = 'calendar'; - protected componentTranslate: string; - constructor(translate: TranslateService, appProvider: CoreAppProvider, courseProvider: CoreCourseProvider, @@ -54,8 +52,6 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { super('AddonCalendarSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils); - - this.componentTranslate = this.translate.instant('addon.calendar.calendarevent'); } /** @@ -66,7 +62,7 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. */ syncAllEvents(siteId?: string, force?: boolean): Promise { - return this.syncOnSites('all calendars', this.syncAllEventsFunc.bind(this), [force], siteId); + return this.syncOnSites('all calendar events', this.syncAllEventsFunc.bind(this), [force], siteId); } /** @@ -77,6 +73,7 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. */ protected syncAllEventsFunc(siteId: string, force?: boolean): Promise { + const promise = force ? this.syncEvents(siteId) : this.syncEventsIfNeeded(siteId); return promise.then((result) => { @@ -196,7 +193,8 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { if (this.syncProvider.isBlocked(AddonCalendarProvider.COMPONENT, event.id, siteId)) { this.logger.debug('Cannot sync event ' + event.name + ' because it is blocked.'); - return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate})); + return Promise.reject(this.translate.instant('core.errorsyncblocked', + {$a: this.translate.instant('addon.calendar.calendarevent')})); } // Try to send the data. @@ -216,7 +214,7 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { return this.calendarOffline.deleteEvent(event.id, siteId).then(() => { // Event deleted, add a warning. result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { - component: this.componentTranslate, + component: this.translate.instant('addon.calendar.calendarevent'), name: event.name, error: this.textUtils.getErrorMessageFromError(error) })); diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 6e5bc965b56..0b9af1e6b11 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -27,6 +27,7 @@ import { CoreConfigProvider } from '@providers/config'; import { ILocalNotification } from '@ionic-native/local-notifications'; import { SQLiteDB } from '@classes/sqlitedb'; import { AddonCalendarOfflineProvider } from './calendar-offline'; +import { TranslateService } from '@ngx-translate/core'; /** * Service to handle calendar events. @@ -40,6 +41,7 @@ export class AddonCalendarProvider { static DEFAULT_NOTIFICATION_TIME = 60; static NEW_EVENT_EVENT = 'addon_calendar_new_event'; static NEW_EVENT_DISCARDED_EVENT = 'addon_calendar_new_event_discarded'; + static EDIT_EVENT_EVENT = 'addon_calendar_edit_event'; static TYPE_CATEGORY = 'category'; static TYPE_COURSE = 'course'; static TYPE_GROUP = 'group'; @@ -218,7 +220,7 @@ export class AddonCalendarProvider { private coursesProvider: CoreCoursesProvider, private timeUtils: CoreTimeUtilsProvider, private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider, private utils: CoreUtilsProvider, private calendarOffline: AddonCalendarOfflineProvider, - private appProvider: CoreAppProvider) { + private appProvider: CoreAppProvider, private translate: TranslateService) { this.logger = logger.getInstance('AddonCalendarProvider'); this.sitesProvider.registerSiteSchema(this.siteSchema); } @@ -980,7 +982,12 @@ export class AddonCalendarProvider { formData.userid = site.getUserId(); formData.visible = 1; formData.instance = 0; - formData['_qf__core_calendar_local_event_forms_create'] = 1; + + if (eventId > 0) { + formData['_qf__core_calendar_local_event_forms_update'] = 1; + } else { + formData['_qf__core_calendar_local_event_forms_create'] = 1; + } const params = { formdata: this.utils.objectToGetParams(formData) @@ -988,7 +995,11 @@ export class AddonCalendarProvider { return site.write('core_calendar_submit_create_update_form', params).then((result) => { if (result.validationerror) { - return Promise.reject(this.utils.createFakeWSError('')); + // Simulate a WS error. + return Promise.reject({ + message: this.translate.instant('core.invalidformdata'), + errorcode: 'validationerror' + }); } return result.event; diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index a7884b46f42..e8a1c8227c0 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1484,6 +1484,7 @@ "core.image": "Image", "core.imageviewer": "Image viewer", "core.info": "Information", + "core.invalidformdata": "Incorrect form data", "core.ios": "iOS", "core.labelsep": ":", "core.lastaccess": "Last access", diff --git a/src/lang/en.json b/src/lang/en.json index b5c79cb94c4..4ec6a60a48e 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -117,6 +117,7 @@ "image": "Image", "imageviewer": "Image viewer", "info": "Information", + "invalidformdata": "Incorrect form data", "ios": "iOS", "labelsep": ":", "lastaccess": "Last access", diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index 0f9f71b3b3c..95cd5cee117 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -1058,7 +1058,7 @@ export class CoreUtilsProvider { * Convert an object to a format of GET param. E.g.: {a: 1, b: 2} -> a=1&b=2 * * @param {any} object Object to convert. - * @param {boolean} [removeEmpty=true] Whether to remove params whose value is empty/null/undefined. + * @param {boolean} [removeEmpty=true] Whether to remove params whose value is null/undefined. * @return {string} GET params. */ objectToGetParams(object: any, removeEmpty: boolean = true): string { @@ -1070,7 +1070,7 @@ export class CoreUtilsProvider { for (const name in flattened) { let value = flattened[name]; - if (removeEmpty && (value === null || typeof value == 'undefined' || value === '')) { + if (removeEmpty && (value === null || typeof value == 'undefined')) { continue; } From 089c56b56bd75135d323e3b0d00eb3ae87628d35 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 26 Jun 2019 15:19:27 +0200 Subject: [PATCH 060/241] MOBILE-3087 calendar: Fix 'lost' events when loading more events When loading more events, it could happen that some events weren't displayed because the timestart was recalculated using the time the request was made. E.g. if I loaded the first events and, 2 minutes later, I loaded more events, there were 2 minutes where we didn't get events. --- src/addon/calendar/pages/list/list.ts | 9 +++++++-- src/addon/calendar/providers/calendar.ts | 19 +++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index 3418f051484..029234a746f 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -21,6 +21,7 @@ import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSitesProvider } from '@providers/sites'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; @@ -43,6 +44,7 @@ export class AddonCalendarListPage implements OnDestroy { @ViewChild(Content) content: Content; @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; + protected initialTime = 0; protected daysLoaded = 0; protected emptyEventsTimes = 0; // Variable to identify consecutive calls returning 0 events. protected categoriesRetrieved = false; @@ -87,7 +89,7 @@ export class AddonCalendarListPage implements OnDestroy { localNotificationsProvider: CoreLocalNotificationsProvider, private popoverCtrl: PopoverController, private eventsProvider: CoreEventsProvider, private navCtrl: NavController, private appProvider: CoreAppProvider, private calendarOffline: AddonCalendarOfflineProvider, private calendarSync: AddonCalendarSyncProvider, - network: Network) { + network: Network, private timeUtils: CoreTimeUtilsProvider) { this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId(); this.notificationsEnabled = localNotificationsProvider.isAvailable(); @@ -192,6 +194,7 @@ export class AddonCalendarListPage implements OnDestroy { * @return {Promise} Promise resolved when done. */ fetchData(refresh?: boolean, sync?: boolean, showErrors?: boolean): Promise { + this.initialTime = this.timeUtils.timestamp(); this.daysLoaded = 0; this.emptyEventsTimes = 0; this.isOnline = this.appProvider.isOnline(); @@ -269,7 +272,9 @@ export class AddonCalendarListPage implements OnDestroy { fetchEvents(refresh?: boolean): Promise { this.loadMoreError = false; - return this.calendarProvider.getEventsList(this.daysLoaded, AddonCalendarProvider.DAYS_INTERVAL).then((events) => { + return this.calendarProvider.getEventsList(this.initialTime, this.daysLoaded, AddonCalendarProvider.DAYS_INTERVAL) + .then((events) => { + this.daysLoaded += AddonCalendarProvider.DAYS_INTERVAL; if (events.length === 0) { this.emptyEventsTimes++; diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 0b9af1e6b11..c04ebaa2ae1 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -537,16 +537,20 @@ export class AddonCalendarProvider { * Get the events in a certain period. The period is calculated like this: * start time: now + daysToStart * end time: start time + daysInterval - * E.g. using provider.getEventsList(30, 30) is going to get the events starting after 30 days from now + * E.g. using provider.getEventsList(undefined, 30, 30) is going to get the events starting after 30 days from now * and ending before 60 days from now. * - * @param {number} [daysToStart=0] Number of days from now to start getting events. + * @param {number} [initialTime] Timestamp when the first fetch was done. If not defined, current time. + * @param {number} [daysToStart=0] Number of days from now to start getting events. * @param {number} [daysInterval=30] Number of days between timestart and timeend. * @param {string} [siteId] Site to get the events from. If not defined, use current site. * @return {Promise} Promise to be resolved when the participants are retrieved. */ - getEventsList(daysToStart: number = 0, daysInterval: number = AddonCalendarProvider.DAYS_INTERVAL, siteId?: string) - : Promise { + getEventsList(initialTime?: number, daysToStart: number = 0, daysInterval: number = AddonCalendarProvider.DAYS_INTERVAL, + siteId?: string): Promise { + + initialTime = initialTime || this.timeUtils.timestamp(); + return this.sitesProvider.getSite(siteId).then((site) => { siteId = site.getId(); const promises = []; @@ -561,9 +565,8 @@ export class AddonCalendarProvider { })); return Promise.all(promises).then(() => { - const now = this.timeUtils.timestamp(), - start = now + (CoreConstants.SECONDS_DAY * daysToStart), - end = start + (CoreConstants.SECONDS_DAY * daysInterval), + const start = initialTime + (CoreConstants.SECONDS_DAY * daysToStart), + end = start + (CoreConstants.SECONDS_DAY * daysInterval) - 1, data = { options: { userevents: 1, @@ -733,7 +736,7 @@ export class AddonCalendarProvider { return this.isDisabled(siteId).then((disabled) => { if (!disabled) { // Get first events. - return this.getEventsList(undefined, undefined, siteId).then((events) => { + return this.getEventsList(undefined, undefined, undefined, siteId).then((events) => { return this.scheduleEventsNotifications(events, siteId); }); } From 649ad7a1a2cc67de0eb97f8964992615f1c9691d Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 27 Jun 2019 10:29:24 +0200 Subject: [PATCH 061/241] MOBILE-3087 calendar: Display offline events in their right position --- .../calendar/pages/edit-event/edit-event.html | 2 +- .../calendar/pages/edit-event/edit-event.ts | 11 +- src/addon/calendar/pages/list/list.html | 18 +-- src/addon/calendar/pages/list/list.ts | 106 ++++++++++++++---- src/addon/calendar/providers/calendar.ts | 1 + 5 files changed, 100 insertions(+), 38 deletions(-) diff --git a/src/addon/calendar/pages/edit-event/edit-event.html b/src/addon/calendar/pages/edit-event/edit-event.html index 96edde1b9cf..7330b752ee8 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.html +++ b/src/addon/calendar/pages/edit-event/edit-event.html @@ -9,7 +9,7 @@ -
+

{{ 'addon.calendar.eventname' | translate }}

diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index e800800c2fb..0017a81ea32 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -71,6 +71,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { protected types: any; // Object with the supported types. protected showAll: boolean; protected isDestroyed = false; + protected error = false; constructor(navParams: NavParams, private navCtrl: NavController, @@ -146,6 +147,8 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { protected fetchData(refresh?: boolean): Promise { let accessInfo; + this.error = false; + // Get access info. return this.calendarProvider.getAccessInformation(this.courseId).then((info) => { accessInfo = info; @@ -254,8 +257,12 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'Error getting data.'); - this.originalData = null; // Avoid asking for confirmation. - this.navCtrl.pop(); + this.error = true; + + if (!this.svComponent || !this.svComponent.isOn()) { + this.originalData = null; // Avoid asking for confirmation. + this.navCtrl.pop(); + } }); } diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index 57563190d00..a224599f7dc 100644 --- a/src/addon/calendar/pages/list/list.html +++ b/src/addon/calendar/pages/list/list.html @@ -26,20 +26,6 @@ - - - {{ 'core.notsent' | translate }} - - -

-

- {{ event.timestart * 1000 | coreFormatDate: "strftimedatetimeshort" }} - - {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimedatetimeshort" }} -

-
-
-
- @@ -54,6 +40,10 @@

- {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimetime" }} - {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimedatetimeshort" }}

+ + + {{ 'core.notsent' | translate }} +
diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index 029234a746f..e3ce88a4467 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -31,6 +31,7 @@ import { CoreAppProvider } from '@providers/app'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import * as moment from 'moment'; import { Network } from '@ionic-native/network'; +import { CoreConstants } from '@core/constants'; /** * Page that displays the list of calendar events. @@ -69,7 +70,8 @@ export class AddonCalendarListPage implements OnDestroy { courses: any[]; eventsLoaded = false; - events = []; + events = []; // Events (both online and offline). + onlineEvents = []; offlineEvents = []; notificationsEnabled = false; filteredEvents = []; @@ -98,7 +100,7 @@ export class AddonCalendarListPage implements OnDestroy { if (this.notificationsEnabled) { // Re-schedule events if default time changes. this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => { - calendarProvider.scheduleEventsNotifications(this.events); + calendarProvider.scheduleEventsNotifications(this.onlineEvents); }, this.currentSiteId); } @@ -179,8 +181,12 @@ export class AddonCalendarListPage implements OnDestroy { this.fetchData(false, true, false).then(() => { if (!this.eventId && this.splitviewCtrl.isOn() && this.events.length > 0) { - // Take first and load it. - this.gotoEvent(this.events[0].id); + // Take first online event and load it. If no online event, load the first offline. + if (this.onlineEvents[0]) { + this.gotoEvent(this.onlineEvents[0].id); + } else { + this.gotoEvent(this.offlineEvents[0].id); + } } }); } @@ -252,8 +258,11 @@ export class AddonCalendarListPage implements OnDestroy { this.hasOffline = !!events.length; // Format data and sort by timestart. - events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); - this.offlineEvents = events.sort((a, b) => a.timestart - b.timestart); + events.forEach((event) => { + event.offline = true; + this.calendarHelper.formatEventData(event); + }); + this.offlineEvents = this.sortEvents(events); })); return Promise.all(promises); @@ -273,39 +282,37 @@ export class AddonCalendarListPage implements OnDestroy { this.loadMoreError = false; return this.calendarProvider.getEventsList(this.initialTime, this.daysLoaded, AddonCalendarProvider.DAYS_INTERVAL) - .then((events) => { + .then((onlineEvents) => { - this.daysLoaded += AddonCalendarProvider.DAYS_INTERVAL; - if (events.length === 0) { + if (onlineEvents.length === 0) { this.emptyEventsTimes++; if (this.emptyEventsTimes > 5) { // Stop execution if we retrieve empty list 6 consecutive times. this.canLoadMore = false; if (refresh) { - this.events = []; + this.onlineEvents = []; this.filteredEvents = []; + this.events = this.offlineEvents; } } else { // No events returned, load next events. + this.daysLoaded += AddonCalendarProvider.DAYS_INTERVAL; + return this.fetchEvents(); } } else { - events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + onlineEvents.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); - // Sort the events by timestart, they're ordered by id. - events.sort((a, b) => { - if (a.timestart == b.timestart) { - return a.timeduration - b.timeduration; - } + // Get the merged events of this period. + const events = this.mergeEvents(onlineEvents); - return a.timestart - b.timestart; - }); - - this.getCategories = this.shouldLoadCategories(events); + this.getCategories = this.shouldLoadCategories(onlineEvents); if (refresh) { + this.onlineEvents = onlineEvents; this.events = events; } else { // Filter events with same ID. Repeated events are returned once per WS call, show them only once. + this.onlineEvents = this.utils.mergeArraysWithoutDuplicates(this.onlineEvents, onlineEvents, 'id'); this.events = this.utils.mergeArraysWithoutDuplicates(this.events, events, 'id'); } this.filteredEvents = this.getFilteredEvents(); @@ -318,7 +325,9 @@ export class AddonCalendarListPage implements OnDestroy { this.canLoadMore = true; // Schedule notifications for the events retrieved (might have new events). - this.calendarProvider.scheduleEventsNotifications(this.events); + this.calendarProvider.scheduleEventsNotifications(this.onlineEvents); + + this.daysLoaded += AddonCalendarProvider.DAYS_INTERVAL; } // Resize the content so infinite loading is able to calculate if it should load more items or not. @@ -441,6 +450,61 @@ export class AddonCalendarListPage implements OnDestroy { }); } + /** + * Merge a period of online events with the offline events of that period. + * + * @param {any[]} onlineEvents Online events. + * @return {any[]} Merged events. + */ + protected mergeEvents(onlineEvents: any[]): any[] { + if (!this.offlineEvents || !this.offlineEvents.length) { + // No offline events, nothing to merge. + return onlineEvents; + } + + const start = this.initialTime + (CoreConstants.SECONDS_DAY * this.daysLoaded), + end = start + (CoreConstants.SECONDS_DAY * AddonCalendarProvider.DAYS_INTERVAL) - 1; + + // First of all, remove the online events that were modified in offline. + let result = onlineEvents.filter((event) => { + const offlineEvent = this.offlineEvents.find((ev) => { + return ev.id == event.id; + }); + + return !offlineEvent; + }); + + // Now get the offline events that belong to this period. + const periodOfflineEvents = this.offlineEvents.filter((event) => { + if (this.daysLoaded == 0 && event.timestart < start) { + // Display offline events that are previous to current time to allow editing them. + return true; + } + + return (event.timestart >= start || event.timestart + event.timeduration >= start) && event.timestart <= end; + }); + + // Merge both arrays and sort them. + result = result.concat(periodOfflineEvents); + + return this.sortEvents(result); + } + + /** + * Sort events by timestart. + * + * @param {any[]} events List to sort. + */ + protected sortEvents(events: any[]): any[] { + return events.sort((a, b) => { + if (a.timestart == b.timestart) { + return a.timeduration - b.timeduration; + } + + return a.timestart - b.timestart; + }); + } + /** * Refresh the data. * diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index c04ebaa2ae1..bfd3a03df70 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -591,6 +591,7 @@ export class AddonCalendarProvider { const preSets = { cacheKey: this.getEventsListCacheKey(daysToStart, daysInterval), getCacheUsingCacheKey: true, + uniqueCacheKey: true, updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; From 12ce9f63b51bd2e735880a39b8eef0908395a407 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 27 Jun 2019 12:34:08 +0200 Subject: [PATCH 062/241] MOBILE-3087 calendar: Support repeateditall setting --- scripts/langindex.json | 4 ++ src/addon/calendar/lang/en.json | 4 ++ .../calendar/pages/edit-event/edit-event.html | 39 +++++++++++++------ .../calendar/pages/edit-event/edit-event.scss | 2 +- .../calendar/pages/edit-event/edit-event.ts | 38 ++++++++++++++---- .../calendar/providers/calendar-offline.ts | 10 +++++ src/assets/lang/en.json | 4 ++ 7 files changed, 81 insertions(+), 20 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 6103e27f7da..75ad6694893 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -96,6 +96,7 @@ "addon.calendar.errorloadevents": "local_moodlemobileapp", "addon.calendar.eventduration": "calendar", "addon.calendar.eventendtime": "calendar", + "addon.calendar.eventkind": "calendar", "addon.calendar.eventname": "calendar", "addon.calendar.eventstarttime": "calendar", "addon.calendar.eventtype": "calendar", @@ -106,6 +107,9 @@ "addon.calendar.noevents": "local_moodlemobileapp", "addon.calendar.nopermissiontoupdatecalendar": "error", "addon.calendar.reminders": "local_moodlemobileapp", + "addon.calendar.repeatedevents": "calendar", + "addon.calendar.repeateditall": "calendar", + "addon.calendar.repeateditthis": "calendar", "addon.calendar.repeatevent": "calendar", "addon.calendar.repeatweeksl": "calendar", "addon.calendar.setnewreminder": "local_moodlemobileapp", diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index a7a53183650..d5cf329a20b 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -12,6 +12,7 @@ "errorloadevents": "Error loading events.", "eventduration": "Duration", "eventendtime": "End time", + "eventkind": "Type of event", "eventname": "Event title", "eventstarttime": "Start time", "eventtype": "Event type", @@ -22,6 +23,9 @@ "noevents": "There are no events", "nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event", "reminders": "Reminders", + "repeatedevents": "Repeated events", + "repeateditall": "Also apply changes to the other {{$a}} events in this repeat series", + "repeateditthis": "Apply changes to this event only", "repeatevent": "Repeat this event", "repeatweeksl": "Repeat weekly, creating altogether", "setnewreminder": "Set a new reminder", diff --git a/src/addon/calendar/pages/edit-event/edit-event.html b/src/addon/calendar/pages/edit-event/edit-event.html index 7330b752ee8..f57a84915ef 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.html +++ b/src/addon/calendar/pages/edit-event/edit-event.html @@ -26,7 +26,7 @@ -

{{ 'addon.calendar.eventtype' | translate }}

+

{{ 'addon.calendar.eventkind' | translate }}

{{ type.name | translate }} @@ -96,8 +96,8 @@
-
-

{{ 'addon.calendar.eventduration' | translate }}

+
+

{{ 'addon.calendar.eventduration' | translate }}

{{ 'addon.calendar.durationnone' | translate }} @@ -118,15 +118,30 @@
- - -

{{ 'addon.calendar.repeatevent' | translate }}

- -
- -

{{ 'addon.calendar.repeatweeksl' | translate }}

- -
+ + + +

{{ 'addon.calendar.repeatevent' | translate }}

+ +
+ +

{{ 'addon.calendar.repeatweeksl' | translate }}

+ +
+
+ + +
+

{{ 'addon.calendar.repeatedevents' | translate }}

+ + {{ 'addon.calendar.repeateditall' | translate:{$a: event.othereventscount} }} + + + + {{ 'addon.calendar.repeateditthis' | translate }} + + +
diff --git a/src/addon/calendar/pages/edit-event/edit-event.scss b/src/addon/calendar/pages/edit-event/edit-event.scss index 3c43c635ef5..6426ce3f192 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.scss +++ b/src/addon/calendar/pages/edit-event/edit-event.scss @@ -1,5 +1,5 @@ ion-app.app-root page-addon-calendar-edit-event { - .addon-calendar-duration-container ion-item:not(.addon-calendar-duration-title) { + .addon-calendar-radio-container ion-item:not(.addon-calendar-radio-title) { &.item-ios { @include padding-horizontal($item-ios-padding-start * 2, null); diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index 0017a81ea32..97049a0342f 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -57,6 +57,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { courseGroupSet = false; advanced = false; errors: any; + event: any; // The event object (when editing an event). // Form variables. eventForm: FormGroup; @@ -72,6 +73,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { protected showAll: boolean; protected isDestroyed = false; protected error = false; + protected gotEventData = false; constructor(navParams: NavParams, private navCtrl: NavController, @@ -126,6 +128,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { this.eventForm.addControl('timedurationminutes', this.fb.control('')); this.eventForm.addControl('repeat', this.fb.control(false)); this.eventForm.addControl('repeats', this.fb.control('1')); + this.eventForm.addControl('repeateditall', this.fb.control(1)); } /** @@ -164,7 +167,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { return Promise.reject(this.translate.instant('addon.calendar.nopermissiontoupdatecalendar')); } - if (this.eventId && !refresh) { + if (this.eventId && !this.gotEventData) { // Editing an event, get the event data. Wait for sync first. promises.push(this.calendarSync.waitForSync(AddonCalendarSyncProvider.SYNC_ID).then(() => { @@ -173,20 +176,35 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { this.syncProvider.blockOperation(AddonCalendarProvider.COMPONENT, this.eventId); } + const promises = []; + // Get the event offline data if there's any. - return this.calendarOffline.getEvent(this.eventId).then((event) => { + promises.push(this.calendarOffline.getEvent(this.eventId).then((event) => { this.hasOffline = true; return event; }).catch(() => { // No offline data. this.hasOffline = false; + })); + + if (this.eventId > 0) { + // It's an online event. get its data from server. + promises.push(this.calendarProvider.getEventById(this.eventId).then((event) => { + this.event = event; + if (event && event.repeatid) { + event.othereventscount = event.eventcount ? event.eventcount - 1 : ''; + } + + return event; + })); + } + + return Promise.all(promises).then((result) => { + this.gotEventData = true; + + const event = result[0] || result[1]; // Use offline data first. - if (this.eventId > 0) { - // It's an online event. get its data from server. - return this.calendarProvider.getEventById(this.eventId); - } - }).then((event) => { if (event) { // Load the data in the form. this.eventForm.controls.name.setValue(event.name); @@ -204,6 +222,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { this.eventForm.controls.timedurationminutes.setValue(event.timedurationminutes || ''); this.eventForm.controls.repeat.setValue(!!event.repeat); this.eventForm.controls.repeats.setValue(event.repeats || '1'); + this.eventForm.controls.repeateditall.setValue(event.repeateditall || 1); } }); })); @@ -394,6 +413,11 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { data.repeats = formData.repeats; } + if (this.event && this.event.repeatid) { + data.repeatid = this.event.repeatid; + data.repeateditall = formData.repeateditall; + } + // Send the data. const modal = this.domUtils.showModalLoading('core.sending', true); diff --git a/src/addon/calendar/providers/calendar-offline.ts b/src/addon/calendar/providers/calendar-offline.ts index 833ca7d4be0..a929113bc1a 100644 --- a/src/addon/calendar/providers/calendar-offline.ts +++ b/src/addon/calendar/providers/calendar-offline.ts @@ -94,6 +94,14 @@ export class AddonCalendarOfflineProvider { name: 'repeats', type: 'TEXT', }, + { + name: 'repeatid', + type: 'INTEGER', + }, + { + name: 'repeateditall', + type: 'INTEGER', + }, { name: 'userid', type: 'INTEGER', @@ -202,6 +210,8 @@ export class AddonCalendarOfflineProvider { timedurationminutes: data.timedurationminutes, repeat: data.repeat ? 1 : 0, repeats: data.repeats, + repeatid: data.repeatid, + repeateditall: data.repeateditall ? 1 : 0, timecreated: timeCreated, userid: site.getUserId() }; diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index e8a1c8227c0..63974e50335 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -96,6 +96,7 @@ "addon.calendar.errorloadevents": "Error loading events.", "addon.calendar.eventduration": "Duration", "addon.calendar.eventendtime": "End time", + "addon.calendar.eventkind": "Type of event", "addon.calendar.eventname": "Event title", "addon.calendar.eventstarttime": "Start time", "addon.calendar.eventtype": "Event type", @@ -106,6 +107,9 @@ "addon.calendar.noevents": "There are no events", "addon.calendar.nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event", "addon.calendar.reminders": "Reminders", + "addon.calendar.repeatedevents": "Repeated events", + "addon.calendar.repeateditall": "Also apply changes to the other {{$a}} events in this repeat series", + "addon.calendar.repeateditthis": "Apply changes to this event only", "addon.calendar.repeatevent": "Repeat this event", "addon.calendar.repeatweeksl": "Repeat weekly, creating altogether", "addon.calendar.setnewreminder": "Set a new reminder", From a9d62274c9489d342ddc4d8225d182b7234ab25f Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 3 Jul 2019 15:04:05 +0200 Subject: [PATCH 063/241] MOBILE-3087 calendar: Fix issues when editing existing events --- .../calendar/pages/edit-event/edit-event.html | 6 +- .../calendar/pages/edit-event/edit-event.ts | 119 ++++++++++++++---- src/addon/calendar/pages/event/event.html | 4 +- 3 files changed, 98 insertions(+), 31 deletions(-) diff --git a/src/addon/calendar/pages/edit-event/edit-event.html b/src/addon/calendar/pages/edit-event/edit-event.html index f57a84915ef..9d79a7e5d9f 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.html +++ b/src/addon/calendar/pages/edit-event/edit-event.html @@ -1,6 +1,6 @@ - + {{ title | translate }} @@ -44,7 +44,7 @@

{{ 'core.course' | translate }}

- + {{ course.fullname }}
@@ -54,7 +54,7 @@

{{ 'core.course' | translate }}

- + {{ course.fullname }}
diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index 97049a0342f..edda98102c8 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -21,6 +21,7 @@ import { CoreGroupsProvider } from '@providers/groups'; import { CoreSitesProvider } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; @@ -79,6 +80,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { private navCtrl: NavController, private translate: TranslateService, private domUtils: CoreDomUtilsProvider, + private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider, private eventsProvider: CoreEventsProvider, private groupsProvider: CoreGroupsProvider, @@ -207,22 +209,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { if (event) { // Load the data in the form. - this.eventForm.controls.name.setValue(event.name); - this.eventForm.controls.timestart.setValue(this.timeUtils.toDatetimeFormat(event.timestart * 1000)); - this.eventForm.controls.eventtype.setValue(event.eventtype); - this.eventForm.controls.categoryid.setValue(event.categoryid || ''); - this.eventForm.controls.courseid.setValue(event.courseid || ''); - this.eventForm.controls.groupcourseid.setValue(event.groupcourseid || ''); - this.eventForm.controls.groupid.setValue(event.groupid || ''); - this.eventForm.controls.description.setValue(event.description); - this.eventForm.controls.location.setValue(event.location); - this.eventForm.controls.duration.setValue(event.duration); - this.eventForm.controls.timedurationuntil.setValue( - this.timeUtils.toDatetimeFormat((event.timedurationuntil * 1000) || Date.now())); - this.eventForm.controls.timedurationminutes.setValue(event.timedurationminutes || ''); - this.eventForm.controls.repeat.setValue(!!event.repeat); - this.eventForm.controls.repeats.setValue(event.repeats || '1'); - this.eventForm.controls.repeateditall.setValue(event.repeateditall || 1); + return this.loadEventData(event, !!result[0]); } }); })); @@ -251,12 +238,24 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { }); } - // Sort courses by name. - this.courses = courses.sort((a, b) => { - const compareA = a.fullname.toLowerCase(), - compareB = b.fullname.toLowerCase(); + // Format the name of the courses. + const subPromises = []; + courses.forEach((course) => { + subPromises.push(this.textUtils.formatText(course.fullname).then((text) => { + course.fullname = text; + }).catch(() => { + // Ignore errors. + })); + }); + + return Promise.all(subPromises).then(() => { + // Sort courses by name. + this.courses = courses.sort((a, b) => { + const compareA = a.fullname.toLowerCase(), + compareB = b.fullname.toLowerCase(); - return compareA.localeCompare(compareB); + return compareA.localeCompare(compareB); + }); }); })); } @@ -285,6 +284,61 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { }); } + /** + * Load an event data into the form. + * + * @param {any} event Event data. + * @param {boolean} isOffline Whether the data is from offline or not. + * @return {Promise} Promise resolved when done. + */ + protected loadEventData(event: any, isOffline: boolean): Promise { + const courseId = event.course ? event.course.id : event.courseid; + + this.eventForm.controls.name.setValue(event.name); + this.eventForm.controls.timestart.setValue(this.timeUtils.toDatetimeFormat(event.timestart * 1000)); + this.eventForm.controls.eventtype.setValue(event.eventtype); + this.eventForm.controls.categoryid.setValue(event.categoryid || ''); + this.eventForm.controls.courseid.setValue(courseId || ''); + this.eventForm.controls.groupcourseid.setValue(event.groupcourseid || courseId || ''); + this.eventForm.controls.groupid.setValue(event.groupid || ''); + this.eventForm.controls.description.setValue(event.description); + this.eventForm.controls.location.setValue(event.location); + + if (isOffline) { + // It's an offline event, use the data as it is. + this.eventForm.controls.duration.setValue(event.duration); + this.eventForm.controls.timedurationuntil.setValue( + this.timeUtils.toDatetimeFormat((event.timedurationuntil * 1000) || Date.now())); + this.eventForm.controls.timedurationminutes.setValue(event.timedurationminutes || ''); + this.eventForm.controls.repeat.setValue(!!event.repeat); + this.eventForm.controls.repeats.setValue(event.repeats || '1'); + this.eventForm.controls.repeateditall.setValue(event.repeateditall || 1); + } else { + // Online event, we'll have to calculate the data. + + if (event.timeduration > 0) { + this.eventForm.controls.duration.setValue(1); + this.eventForm.controls.timedurationuntil.setValue(this.timeUtils.toDatetimeFormat( + (event.timestart + event.timeduration) * 1000)); + } else { + // No duration. + this.eventForm.controls.duration.setValue(0); + this.eventForm.controls.timedurationuntil.setValue(this.timeUtils.toDatetimeFormat()); + } + + this.eventForm.controls.timedurationminutes.setValue(''); + this.eventForm.controls.repeat.setValue(!!event.repeatid); + this.eventForm.controls.repeats.setValue(event.eventcount || '1'); + this.eventForm.controls.repeateditall.setValue(1); + } + + if (event.eventtype == 'group' && courseId) { + return this.loadGroups(courseId); + } + + return Promise.resolve(); + } + /** * Pull to refresh. * @@ -327,20 +381,33 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { } const modal = this.domUtils.showModalLoading(); - this.loadingGroups = true; - this.groupsProvider.getUserGroupsInCourse(courseId).then((groups) => { - this.groups = groups; - this.courseGroupSet = true; + this.loadGroups(courseId).then(() => { this.groupControl.setValue(''); }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'Error getting data.'); }).finally(() => { - this.loadingGroups = false; modal.dismiss(); }); } + /** + * Load groups of a certain course. + * + * @param {number} courseId Course ID. + * @return {Promise} Promise resolved when done. + */ + protected loadGroups(courseId: number): Promise { + this.loadingGroups = true; + + return this.groupsProvider.getUserGroupsInCourse(courseId).then((groups) => { + this.groups = groups; + this.courseGroupSet = true; + }).finally(() => { + this.loadingGroups = false; + }); + } + /** * Show or hide advanced form fields. */ diff --git a/src/addon/calendar/pages/event/event.html b/src/addon/calendar/pages/event/event.html index 93e5bd28507..2608581ae48 100644 --- a/src/addon/calendar/pages/event/event.html +++ b/src/addon/calendar/pages/event/event.html @@ -40,10 +40,10 @@

{{ 'addon.calendar.eventendtime' | translate}}

{{ 'core.course' | translate}}

- +

{{ 'core.group' | translate}}

{{ groupName }}

-
+

{{ 'core.category' | translate}}

From 1d8f61733891b40d9fbf7e7a605b3ae216b7881a Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 28 Jun 2019 11:42:35 +0200 Subject: [PATCH 064/241] MOBILE-3090 calendar: Support deleting events --- scripts/langindex.json | 6 + src/addon/calendar/lang/en.json | 6 + src/addon/calendar/pages/event/event.html | 11 +- src/addon/calendar/pages/event/event.scss | 5 + src/addon/calendar/pages/event/event.ts | 135 +++++++++++++- src/addon/calendar/pages/list/list.html | 8 +- src/addon/calendar/pages/list/list.scss | 5 + src/addon/calendar/pages/list/list.ts | 108 +++++++++-- .../calendar/providers/calendar-offline.ts | 167 +++++++++++++++++- src/addon/calendar/providers/calendar-sync.ts | 122 +++++++++---- src/addon/calendar/providers/calendar.ts | 107 ++++++++++- src/addon/mod/chat/pages/chat/chat.ts | 2 +- src/addon/mod/chat/pages/users/users.ts | 2 +- src/addon/mod/feedback/pages/form/form.ts | 4 +- .../mod/forum/pages/discussion/discussion.ts | 2 +- src/assets/lang/en.json | 6 + .../course/classes/main-activity-component.ts | 4 +- src/providers/utils/dom.ts | 7 +- 18 files changed, 627 insertions(+), 80 deletions(-) create mode 100644 src/addon/calendar/pages/event/event.scss create mode 100644 src/addon/calendar/pages/list/list.scss diff --git a/scripts/langindex.json b/scripts/langindex.json index 75ad6694893..40cfc905d7d 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -87,13 +87,19 @@ "addon.calendar.calendarevent": "local_moodlemobileapp", "addon.calendar.calendarevents": "local_moodlemobileapp", "addon.calendar.calendarreminders": "local_moodlemobileapp", + "addon.calendar.confirmeventdelete": "calendar", + "addon.calendar.confirmeventseriesdelete": "calendar", "addon.calendar.defaultnotificationtime": "local_moodlemobileapp", + "addon.calendar.deleteallevents": "calendar", + "addon.calendar.deleteevent": "calendar", + "addon.calendar.deleteoneevent": "calendar", "addon.calendar.durationminutes": "calendar", "addon.calendar.durationnone": "calendar", "addon.calendar.durationuntil": "calendar", "addon.calendar.editevent": "calendar", "addon.calendar.errorloadevent": "local_moodlemobileapp", "addon.calendar.errorloadevents": "local_moodlemobileapp", + "addon.calendar.eventcalendareventdeleted": "calendar", "addon.calendar.eventduration": "calendar", "addon.calendar.eventendtime": "calendar", "addon.calendar.eventkind": "calendar", diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index d5cf329a20b..8792b48c52a 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -3,13 +3,19 @@ "calendarevent": "Calendar event", "calendarevents": "Calendar events", "calendarreminders": "Calendar reminders", + "confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?", + "confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?", "defaultnotificationtime": "Default notification time", + "deleteallevents": "Delete all events", + "deleteevent": "Delete event", + "deleteoneevent": "Delete this event", "durationminutes": "Duration in minutes", "durationnone": "Without duration", "durationuntil": "Until", "editevent": "Editing event", "errorloadevent": "Error loading event.", "errorloadevents": "Error loading events.", + "eventcalendareventdeleted": "Calendar event deleted", "eventduration": "Duration", "eventendtime": "End time", "eventkind": "Type of event", diff --git a/src/addon/calendar/pages/event/event.html b/src/addon/calendar/pages/event/event.html index 2608581ae48..23e55aac7ae 100644 --- a/src/addon/calendar/pages/event/event.html +++ b/src/addon/calendar/pages/event/event.html @@ -8,8 +8,10 @@ - - + + + + @@ -18,7 +20,7 @@ - + {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendarevent' | translate} }} @@ -27,6 +29,9 @@

+ + {{ 'core.deletedoffline' | translate }} +

{{ 'addon.calendar.eventstarttime' | translate}}

diff --git a/src/addon/calendar/pages/event/event.scss b/src/addon/calendar/pages/event/event.scss new file mode 100644 index 00000000000..6a991373758 --- /dev/null +++ b/src/addon/calendar/pages/event/event.scss @@ -0,0 +1,5 @@ +ion-app.app-root page-addon-calendar-event { + .card ion-note { + font-size: 1.6rem; + } +} diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index 6fb0a16d11f..9f312a3e807 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -68,6 +68,7 @@ export class AddonCalendarEventPage implements OnDestroy { defaultTime: number; reminders: any[]; canEdit = false; + canDelete = false; hasOffline = false; isOnline = false; syncIcon: string; // Sync icon. @@ -89,8 +90,9 @@ export class AddonCalendarEventPage implements OnDestroy { this.currentSiteId = sitesProvider.getCurrentSiteId(); this.isSplitViewOn = this.svComponent && this.svComponent.isOn(); - // Check if site supports editing. No need to check allowed types, event.canedit already does it. + // Check if site supports editing and deleting. No need to check allowed types, event.canedit already does it. this.canEdit = this.calendarProvider.canEditEventsInSite(); + this.canDelete = this.calendarProvider.canDeleteEventsInSite(); if (this.notificationsEnabled) { this.calendarProvider.getEventReminders(this.eventId).then((reminders) => { @@ -123,10 +125,10 @@ export class AddonCalendarEventPage implements OnDestroy { this.currentSiteId); // Refresh online status when changes. - this.onlineObserver = network.onchange().subscribe((online) => { + this.onlineObserver = network.onchange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { - this.isOnline = online; + this.isOnline = this.appProvider.isOnline(); }); }); } @@ -150,7 +152,8 @@ export class AddonCalendarEventPage implements OnDestroy { fetchEvent(sync?: boolean, showErrors?: boolean): Promise { const currentSite = this.sitesProvider.getCurrentSite(), canGetById = this.calendarProvider.isGetEventByIdAvailable(); - let promise; + let promise, + deleted = false; this.isOnline = this.appProvider.isOnline(); @@ -161,6 +164,11 @@ export class AddonCalendarEventPage implements OnDestroy { this.domUtils.showErrorModal(result.warnings[0]); } + if (result.deleted && result.deleted.indexOf(this.eventId) != -1) { + // This event was deleted during the sync. + deleted = true; + } + if (result.updated) { // Trigger a manual sync event. result.source = 'event'; @@ -177,6 +185,10 @@ export class AddonCalendarEventPage implements OnDestroy { } return promise.then(() => { + if (deleted) { + return; + } + const promises = []; // Get the event data. @@ -204,6 +216,10 @@ export class AddonCalendarEventPage implements OnDestroy { }); }).then((event) => { + if (deleted) { + return; + } + const promises = []; this.calendarHelper.formatEventData(event); @@ -251,7 +267,7 @@ export class AddonCalendarEventPage implements OnDestroy { this.title = title; // If the event belongs to a course, get the course name and the URL to view it. - if (canGetById && event.course) { + if (canGetById && event.course && event.course.id != this.siteHomeId) { this.courseName = event.course.fullname; this.courseUrl = event.course.viewurl; } else if (event.courseid && event.courseid != this.siteHomeId) { @@ -290,6 +306,11 @@ export class AddonCalendarEventPage implements OnDestroy { event.encodedLocation = this.textUtils.buildAddressURL(event.location); } + // Check if event was deleted in offine. + promises.push(this.calendarOffline.isEventDeleted(this.eventId).then((deleted) => { + event.deleted = deleted; + })); + return Promise.all(promises); }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevent', true); @@ -391,6 +412,97 @@ export class AddonCalendarEventPage implements OnDestroy { navCtrl.push('AddonCalendarEditEventPage', {eventId: this.eventId}); } + /** + * Delete the event. + */ + deleteEvent(): void { + const title = this.translate.instant('addon.calendar.deleteevent'), + options: any = {}; + let message: string; + + if (this.event.eventcount > 1) { + // It's a repeated event. + message = this.translate.instant('addon.calendar.confirmeventseriesdelete', + {$a: {name: this.event.name, count: this.event.eventcount}}); + + options.inputs = [ + { + type: 'radio', + name: 'deleteall', + checked: true, + value: false, + label: this.translate.instant('addon.calendar.deleteoneevent') + }, + { + type: 'radio', + name: 'deleteall', + checked: false, + value: true, + label: this.translate.instant('addon.calendar.deleteallevents') + } + ]; + } else { + // Not repeated, display a simple confirm. + message = this.translate.instant('addon.calendar.confirmeventdelete', {$a: this.event.name}); + } + + this.domUtils.showConfirm(message, title, undefined, undefined, options).then((deleteAll) => { + + const modal = this.domUtils.showModalLoading('core.sending', true); + + this.calendarProvider.deleteEvent(this.event.id, this.event.name, deleteAll).then((sent) => { + + // Trigger an event. + this.eventsProvider.trigger(AddonCalendarProvider.DELETED_EVENT_EVENT, { + eventId: this.eventId, + sent: sent + }, this.sitesProvider.getCurrentSiteId()); + + if (sent) { + this.domUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000, undefined, false); + + // Event deleted, close the view. + if (!this.svComponent || !this.svComponent.isOn()) { + this.navCtrl.pop(); + } + } else { + // Event deleted in offline, just mark it as deleted. + this.event.deleted = true; + } + + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error deleting event.'); + }).finally(() => { + modal.dismiss(); + }); + }, () => { + // User canceled. + }); + } + + /** + * Undo delete the event. + */ + undoDelete(): void { + const modal = this.domUtils.showModalLoading('core.sending', true); + + this.calendarOffline.unmarkDeleted(this.event.id).then(() => { + + // Trigger an event. + this.eventsProvider.trigger(AddonCalendarProvider.UNDELETED_EVENT_EVENT, { + eventId: this.eventId + }, this.sitesProvider.getCurrentSiteId()); + + this.domUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000, undefined, false); + this.event.deleted = false; + + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error undeleting event.'); + }).finally(() => { + modal.dismiss(); + }); + } + /** * Check the result of an automatic sync or a manual sync not done by this page. * @@ -398,7 +510,18 @@ export class AddonCalendarEventPage implements OnDestroy { * @param {any} data Sync result. */ protected checkSyncResult(isManual: boolean, data: any): void { - if (data && data.events && (!isManual || data.source != 'event')) { + if (!data) { + return; + } + + if (data.deleted && data.deleted.indexOf(this.eventId) != -1) { + this.domUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000, undefined, false); + + // Event was deleted, close the view. + if (!this.svComponent || !this.svComponent.isOn()) { + this.navCtrl.pop(); + } + } else if (data.events && (!isManual || data.source != 'event')) { const event = data.events.find((ev) => { return ev.id == this.eventId; }); diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index a224599f7dc..bd5c848adab 100644 --- a/src/addon/calendar/pages/list/list.html +++ b/src/addon/calendar/pages/list/list.html @@ -40,9 +40,13 @@

- {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimetime" }} - {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimedatetimeshort" }}

- + - {{ 'core.notsent' | translate }} + {{ 'core.notsent' | translate }} + + + + {{ 'core.deletedoffline' | translate }}
diff --git a/src/addon/calendar/pages/list/list.scss b/src/addon/calendar/pages/list/list.scss new file mode 100644 index 00000000000..9f40d974653 --- /dev/null +++ b/src/addon/calendar/pages/list/list.scss @@ -0,0 +1,5 @@ +ion-app.app-root page-addon-calendar-list { + ion-note { + max-width: 30%; + } +} diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index e3ce88a4467..0dac7f74b54 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -63,16 +63,19 @@ export class AddonCalendarListPage implements OnDestroy { protected newEventObserver: any; protected discardedObserver: any; protected editEventObserver: any; + protected deleteEventObserver: any; + protected undeleteEventObserver: any; protected syncObserver: any; protected manualSyncObserver: any; protected onlineObserver: any; protected currentSiteId: string; + protected onlineEvents = []; + protected offlineEvents = []; + protected deletedEvents = []; courses: any[]; eventsLoaded = false; events = []; // Events (both online and offline). - onlineEvents = []; - offlineEvents = []; notificationsEnabled = false; filteredEvents = []; canLoadMore = false; @@ -149,6 +152,11 @@ export class AddonCalendarListPage implements OnDestroy { this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => { this.eventsLoaded = false; this.refreshEvents(); + + if (this.splitviewCtrl.isOn() && this.eventId && data && data.deleted && data.deleted.indexOf(this.eventId) != -1) { + // Current selected event was deleted. Clear details. + this.splitviewCtrl.emptyDetails(); + } }, this.currentSiteId); // Refresh data if calendar events are synchronized manually but not by this page. @@ -157,13 +165,51 @@ export class AddonCalendarListPage implements OnDestroy { this.eventsLoaded = false; this.refreshEvents(); } + + if (this.splitviewCtrl.isOn() && this.eventId && data && data.deleted && data.deleted.indexOf(this.eventId) != -1) { + // Current selected event was deleted. Clear details. + this.splitviewCtrl.emptyDetails(); + } + }, this.currentSiteId); + + // Update the list when an event is deleted. + this.deleteEventObserver = eventsProvider.on(AddonCalendarProvider.DELETED_EVENT_EVENT, (data) => { + if (data && !data.sent) { + // Event was deleted in offline. Just mark it as deleted, no need to refresh. + this.markAsDeleted(data.eventId, true); + this.hasOffline = true; + } else { + // Event deleted, clear the details if needed and refresh the view. + if (this.splitviewCtrl.isOn()) { + this.splitviewCtrl.emptyDetails(); + } + + this.eventsLoaded = false; + this.refreshEvents(); + } + }, this.currentSiteId); + + // Listen for events "undeleted" (offline). + this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => { + if (data && data.eventId) { + // Mark it as undeleted, no need to refresh. + this.markAsDeleted(data.eventId, false); + + // Remove it from the list of deleted events if it's there. + const index = this.deletedEvents.indexOf(data.eventId); + if (index != -1) { + this.deletedEvents.splice(index, 1); + } + + this.hasOffline = !!this.offlineEvents.length || !!this.deletedEvents.length; + } }, this.currentSiteId); // Refresh online status when changes. - this.onlineObserver = network.onchange().subscribe((online) => { + this.onlineObserver = network.onchange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { - this.isOnline = online; + this.isOnline = this.appProvider.isOnline(); }); }); } @@ -234,6 +280,8 @@ export class AddonCalendarListPage implements OnDestroy { const promises = []; const courseId = this.filter.course.id != this.allCourses.id ? this.filter.course.id : undefined; + this.hasOffline = false; + promises.push(this.calendarHelper.canEditEvents(courseId).then((canEdit) => { this.canCreate = canEdit; })); @@ -254,8 +302,8 @@ export class AddonCalendarListPage implements OnDestroy { })); // Get offline events. - promises.push(this.calendarOffline.getAllEvents().then((events) => { - this.hasOffline = !!events.length; + promises.push(this.calendarOffline.getAllEditedEvents().then((events) => { + this.hasOffline = this.hasOffline || !!events.length; // Format data and sort by timestart. events.forEach((event) => { @@ -265,6 +313,12 @@ export class AddonCalendarListPage implements OnDestroy { this.offlineEvents = this.sortEvents(events); })); + // Get events deleted in offline. + promises.push(this.calendarOffline.getAllDeletedEventsIds().then((ids) => { + this.hasOffline = this.hasOffline || !!ids.length; + this.deletedEvents = ids; + })); + return Promise.all(promises); }).finally(() => { this.eventsLoaded = true; @@ -457,22 +511,32 @@ export class AddonCalendarListPage implements OnDestroy { * @return {any[]} Merged events. */ protected mergeEvents(onlineEvents: any[]): any[] { - if (!this.offlineEvents || !this.offlineEvents.length) { + if (!this.offlineEvents.length && !this.deletedEvents.length) { // No offline events, nothing to merge. return onlineEvents; } const start = this.initialTime + (CoreConstants.SECONDS_DAY * this.daysLoaded), end = start + (CoreConstants.SECONDS_DAY * AddonCalendarProvider.DAYS_INTERVAL) - 1; + let result = onlineEvents; - // First of all, remove the online events that were modified in offline. - let result = onlineEvents.filter((event) => { - const offlineEvent = this.offlineEvents.find((ev) => { - return ev.id == event.id; + if (this.deletedEvents.length) { + // Mark as deleted the events that were deleted in offline. + result.forEach((event) => { + event.deleted = this.deletedEvents.indexOf(event.id) != -1; }); + } - return !offlineEvent; - }); + if (this.offlineEvents.length) { + // Remove the online events that were modified in offline. + result = result.filter((event) => { + const offlineEvent = this.offlineEvents.find((ev) => { + return ev.id == event.id; + }); + + return !offlineEvent; + }); + } // Now get the offline events that belong to this period. const periodOfflineEvents = this.offlineEvents.filter((event) => { @@ -658,6 +722,22 @@ export class AddonCalendarListPage implements OnDestroy { } } + /** + * Find an event and mark it as deleted. + * + * @param {number} eventId Event ID. + * @param {boolean} deleted Whether to mark it as deleted or not. + */ + protected markAsDeleted(eventId: number, deleted: boolean): void { + const event = this.onlineEvents.find((event) => { + return event.id == eventId; + }); + + if (event) { + event.deleted = deleted; + } + } + /** * Page destroyed. */ @@ -666,6 +746,8 @@ export class AddonCalendarListPage implements OnDestroy { this.newEventObserver && this.newEventObserver.off(); this.discardedObserver && this.discardedObserver.off(); this.editEventObserver && this.editEventObserver.off(); + this.deleteEventObserver && this.deleteEventObserver.off(); + this.undeleteEventObserver && this.undeleteEventObserver.off(); this.syncObserver && this.syncObserver.off(); this.manualSyncObserver && this.manualSyncObserver.off(); this.onlineObserver && this.onlineObserver.unsubscribe(); diff --git a/src/addon/calendar/providers/calendar-offline.ts b/src/addon/calendar/providers/calendar-offline.ts index a929113bc1a..3d48baa38a7 100644 --- a/src/addon/calendar/providers/calendar-offline.ts +++ b/src/addon/calendar/providers/calendar-offline.ts @@ -14,6 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; /** * Service to handle offline calendar events. @@ -23,6 +24,7 @@ export class AddonCalendarOfflineProvider { // Variables for database. static EVENTS_TABLE = 'addon_calendar_offline_events'; + static DELETED_EVENTS_TABLE = 'addon_calendar_deleted_events'; protected siteSchema: CoreSiteSchema = { name: 'AddonCalendarOfflineProvider', @@ -34,6 +36,7 @@ export class AddonCalendarOfflineProvider { { name: 'id', // Negative for offline entries. type: 'INTEGER', + primaryKey: true }, { name: 'name', @@ -110,13 +113,35 @@ export class AddonCalendarOfflineProvider { name: 'timecreated', type: 'INTEGER', } - ], - primaryKeys: ['id'] + ] + }, + { + name: AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE, + columns: [ + { + name: 'id', + type: 'INTEGER', + primaryKey: true + }, + { + name: 'name', // Save the name to be able to notify the user. + type: 'TEXT', + notNull: true + }, + { + name: 'repeat', + type: 'INTEGER' + }, + { + name: 'timemodified', + type: 'INTEGER', + } + ] } ] }; - constructor(private sitesProvider: CoreSitesProvider) { + constructor(private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider) { this.sitesProvider.registerSiteSchema(this.siteSchema); } @@ -138,17 +163,91 @@ export class AddonCalendarOfflineProvider { } /** - * Get all offline events. + * Get the IDs of all the events created/edited/deleted in offline. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the IDs. + */ + getAllEventsIds(siteId?: string): Promise { + const promises = []; + + promises.push(this.getAllDeletedEventsIds(siteId)); + promises.push(this.getAllEditedEventsIds(siteId)); + + return Promise.all(promises).then((result) => { + return this.utils.mergeArraysWithoutDuplicates(result[0], result[1]); + }); + } + + /** + * Get all the events deleted in offline. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with all the events deleted in offline. + */ + getAllDeletedEvents(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE); + }); + } + + /** + * Get the IDs of all the events deleted in offline. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the IDs of all the events deleted in offline. + */ + getAllDeletedEventsIds(siteId?: string): Promise { + return this.getAllDeletedEvents(siteId).then((events) => { + return events.map((event) => { + return event.id; + }); + }); + } + + /** + * Get all the events created/edited in offline. * * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with events. */ - getAllEvents(siteId?: string): Promise { + getAllEditedEvents(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.getDb().getRecords(AddonCalendarOfflineProvider.EVENTS_TABLE); }); } + /** + * Get the IDs of all the events created/edited in offline. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with events IDs. + */ + getAllEditedEventsIds(siteId?: string): Promise { + return this.getAllEditedEvents(siteId).then((events) => { + return events.map((event) => { + return event.id; + }); + }); + } + + /** + * Get an event deleted in offline. + * + * @param {number} eventId Event ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the deleted event. + */ + getDeletedEvent(eventId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions: any = { + id: eventId + }; + + return site.getDb().getRecord(AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE, conditions); + }); + } + /** * Get an offline event. * @@ -172,8 +271,8 @@ export class AddonCalendarOfflineProvider { * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with boolean: true if has offline events, false otherwise. */ - hasEvents(siteId?: string): Promise { - return this.getAllEvents(siteId).then((events) => { + hasEditedEvents(siteId?: string): Promise { + return this.getAllEditedEvents(siteId).then((events) => { return !!events.length; }).catch(() => { // No offline data found, return false. @@ -181,6 +280,43 @@ export class AddonCalendarOfflineProvider { }); } + /** + * Check if an event is deleted. + * + * @param {number} eventId Event ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: whether the event is deleted. + */ + isEventDeleted(eventId: number, siteId?: string): Promise { + return this.getDeletedEvent(eventId, siteId).then((event) => { + return !!event; + }).catch(() => { + return false; + }); + } + + /** + * Mark an event as deleted. + * + * @param {number} eventId Event ID to delete. + * @param {number} name Name of the event to delete. + * @param {boolean} [deleteAll] If it's a repeated event. whether to delete all events of the series. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + markDeleted(eventId: number, name: string, deleteAll?: boolean, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const event = { + id: eventId, + name: name || '', + repeat: deleteAll ? 1 : 0, + timemodified: Date.now() + }; + + return site.getDb().insertRecord(AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE, event); + }); + } + /** * Offline version for adding a new discussion to a forum. * @@ -221,4 +357,21 @@ export class AddonCalendarOfflineProvider { }); }); } + + /** + * Unmark an event as deleted. + * + * @param {number} eventId Event ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + unmarkDeleted(eventId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions: any = { + id: eventId + }; + + return site.getDb().deleteRecords(AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE, conditions); + }); + } } diff --git a/src/addon/calendar/providers/calendar-sync.ts b/src/addon/calendar/providers/calendar-sync.ts index 834d1ac34ec..6b81d39ae81 100644 --- a/src/addon/calendar/providers/calendar-sync.ts +++ b/src/addon/calendar/providers/calendar-sync.ts @@ -81,7 +81,8 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { // Sync successful, send event. this.eventsProvider.trigger(AddonCalendarSyncProvider.AUTO_SYNCED, { warnings: result.warnings, - events: result.events + events: result.events, + deleted: result.deleted }, siteId); } }); @@ -122,18 +123,19 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { const result = { warnings: [], events: [], + deleted: [], updated: false }; - let offlineEvents; + let offlineEventIds: number[]; // Get offline events. - const syncPromise = this.calendarOffline.getAllEvents(siteId).catch(() => { + const syncPromise = this.calendarOffline.getAllEventsIds(siteId).catch(() => { // No offline data found, return empty list. return []; - }).then((events) => { - offlineEvents = events; + }).then((eventIds) => { + offlineEventIds = eventIds; - if (!events.length) { + if (!eventIds.length) { // Nothing to sync. return; } else if (!this.appProvider.isOnline()) { @@ -143,8 +145,8 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { const promises = []; - events.forEach((event) => { - promises.push(this.syncOfflineEvent(event, result, siteId)); + offlineEventIds.forEach((eventId) => { + promises.push(this.syncOfflineEvent(eventId, result, siteId)); }); return this.utils.allPromises(promises); @@ -155,10 +157,10 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { this.calendarProvider.invalidateEventsList(siteId), ]; - offlineEvents.forEach((event) => { - if (event.id > 0) { + offlineEventIds.forEach((eventId) => { + if (eventId > 0) { // An event was edited, invalidate its data too. - promises.push(this.calendarProvider.invalidateEvent(event.id, siteId)); + promises.push(this.calendarProvider.invalidateEvent(eventId, siteId)); } }); @@ -182,47 +184,95 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { /** * Synchronize an offline event. * - * @param {any} event The event to sync. + * @param {number} eventId The event ID to sync. * @param {any} result Object where to store the result of the sync. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved if sync is successful, rejected otherwise. */ - protected syncOfflineEvent(event: any, result: any, siteId?: string): Promise { + protected syncOfflineEvent(eventId: number, result: any, siteId?: string): Promise { // Verify that event isn't blocked. - if (this.syncProvider.isBlocked(AddonCalendarProvider.COMPONENT, event.id, siteId)) { - this.logger.debug('Cannot sync event ' + event.name + ' because it is blocked.'); + if (this.syncProvider.isBlocked(AddonCalendarProvider.COMPONENT, eventId, siteId)) { + this.logger.debug('Cannot sync event ' + eventId + ' because it is blocked.'); return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.translate.instant('addon.calendar.calendarevent')})); } - // Try to send the data. - const data = this.utils.clone(event); // Clone the object because it will be modified in the submit function. + // First of all, check if the event has been deleted. + return this.calendarOffline.getDeletedEvent(eventId, siteId).then((data) => { + // Delete the event. + return this.calendarProvider.deleteEventOnline(data.id, data.repeat, siteId).then(() => { + result.updated = true; + result.deleted.push(eventId); - return this.calendarProvider.submitEventOnline(event.id > 0 ? event.id : undefined, data, siteId).then((newEvent) => { - result.updated = true; - result.events.push(newEvent); + // Event sent, delete the offline data. + const promises = []; - // Event sent, delete the offline data. - return this.calendarOffline.deleteEvent(event.id, siteId); - }).catch((error) => { - if (this.utils.isWebServiceError(error)) { - // The WebService has thrown an error, this means that the event cannot be created. Delete it. - result.updated = true; + promises.push(this.calendarOffline.unmarkDeleted(eventId, siteId)); + promises.push(this.calendarOffline.deleteEvent(eventId, siteId).catch(() => { + // Ignore errors, maybe there was no edit data. + })); + + return Promise.all(promises); + }).catch((error) => { + + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that the event cannot be created. Delete it. + result.updated = true; + + const promises = []; - return this.calendarOffline.deleteEvent(event.id, siteId).then(() => { - // Event deleted, add a warning. - result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { - component: this.translate.instant('addon.calendar.calendarevent'), - name: event.name, - error: this.textUtils.getErrorMessageFromError(error) + promises.push(this.calendarOffline.unmarkDeleted(eventId, siteId)); + promises.push(this.calendarOffline.deleteEvent(eventId, siteId).catch(() => { + // Ignore errors, maybe there was no edit data. })); - }); - } - // Local error, reject. - return Promise.reject(error); + return Promise.all(promises).then(() => { + // Event deleted, add a warning. + result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { + component: this.translate.instant('addon.calendar.calendarevent'), + name: data.name, + error: this.textUtils.getErrorMessageFromError(error) + })); + }); + } + + // Local error, reject. + return Promise.reject(error); + }); + }, () => { + + // Not deleted. Now get the event data. + return this.calendarOffline.getEvent(eventId, siteId).then((event) => { + // Try to send the data. + const data = this.utils.clone(event); // Clone the object because it will be modified in the submit function. + + return this.calendarProvider.submitEventOnline(eventId > 0 ? eventId : undefined, data, siteId).then((newEvent) => { + result.updated = true; + result.events.push(newEvent); + + // Event sent, delete the offline data. + return this.calendarOffline.deleteEvent(event.id, siteId); + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that the event cannot be created. Delete it. + result.updated = true; + + return this.calendarOffline.deleteEvent(event.id, siteId).then(() => { + // Event deleted, add a warning. + result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { + component: this.translate.instant('addon.calendar.calendarevent'), + name: event.name, + error: this.textUtils.getErrorMessageFromError(error) + })); + }); + } + + // Local error, reject. + return Promise.reject(error); + }); + }); }); } } diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index bfd3a03df70..9e34520a190 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -42,6 +42,8 @@ export class AddonCalendarProvider { static NEW_EVENT_EVENT = 'addon_calendar_new_event'; static NEW_EVENT_DISCARDED_EVENT = 'addon_calendar_new_event_discarded'; static EDIT_EVENT_EVENT = 'addon_calendar_edit_event'; + static DELETED_EVENT_EVENT = 'addon_calendar_deleted_event'; + static UNDELETED_EVENT_EVENT = 'addon_calendar_undeleted_event'; static TYPE_CATEGORY = 'category'; static TYPE_COURSE = 'course'; static TYPE_GROUP = 'group'; @@ -225,11 +227,38 @@ export class AddonCalendarProvider { this.sitesProvider.registerSiteSchema(this.siteSchema); } + /** + * Check if a certain site allows deleting events. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if can delete. + * @since 3.3 + */ + canDeleteEvents(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.canDeleteEventsInSite(site); + }); + } + + /** + * Check if a certain site allows deleting events. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether events can be deleted. + * @since 3.3 + */ + canDeleteEventsInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.wsAvailable('core_calendar_delete_calendar_events'); + } + /** * Check if a certain site allows creating and editing events. * * @param {string} [siteId] Site Id. If not defined, use current site. * @return {Promise} Promise resolved with true if can create/edit. + * @since 3.7.1 */ canEditEvents(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { @@ -242,6 +271,7 @@ export class AddonCalendarProvider { * * @param {CoreSite} [site] Site. If not defined, use current site. * @return {boolean} Whether events can be created and edited. + * @since 3.7.1 */ canEditEventsInSite(site?: CoreSite): boolean { site = site || this.sitesProvider.getCurrentSite(); @@ -261,20 +291,89 @@ export class AddonCalendarProvider { return site.getDb().getRecordsSelect(AddonCalendarProvider.EVENTS_TABLE, 'timestart + timeduration < ?', [this.timeUtils.timestamp()]).then((events) => { return Promise.all(events.map((event) => { - return this.deleteEvent(event.id, siteId); + return this.deleteLocalEvent(event.id, siteId); })); }); }); } /** - * Delete event cancelling all the reminders and notifications. + * Delete an event. + * + * @param {number} eventId Event ID to delete. + * @param {string} name Name of the event to delete. + * @param {boolean} [deleteAll] If it's a repeated event. whether to delete all events of the series. + * @param {boolean} [forceOffline] True to always save it in offline. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + deleteEvent(eventId: number, name: string, deleteAll?: boolean, forceOffline?: boolean, siteId?: string): Promise { + + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Function to store the submission to be synchronized later. + const storeOffline = (): Promise => { + return this.calendarOffline.markDeleted(eventId, name, deleteAll, siteId).then(() => { + return false; + }); + }; + + if (forceOffline || !this.appProvider.isOnline()) { + // App is offline, store the action. + return storeOffline(); + } + + // If the event is already stored, discard it first. + return this.calendarOffline.unmarkDeleted(eventId, siteId).then(() => { + return this.deleteEventOnline(eventId, deleteAll, siteId).then(() => { + return true; + }).catch((error) => { + if (error && !this.utils.isWebServiceError(error)) { + // Couldn't connect to server, store in offline. + return storeOffline(); + } else { + // The WebService has thrown an error, reject. + return Promise.reject(error); + } + }); + }); + } + + /** + * Delete an event. It will fail if offline or cannot connect. + * + * @param {number} eventId Event ID to delete. + * @param {boolean} [deleteAll] If it's a repeated event. whether to delete all events of the series. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + deleteEventOnline(eventId: number, deleteAll?: boolean, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + + const params = { + events: [ + { + eventid: eventId, + repeat: deleteAll ? 1 : 0 + } + ] + }, + preSets = { + responseExpected: false + }; + + return site.write('core_calendar_delete_calendar_events', params, preSets); + }); + } + + /** + * Delete a locally stored event cancelling all the reminders and notifications. * * @param {number} eventId Event ID. * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. * @return {Promise} Resolved when done. */ - protected deleteEvent(eventId: number, siteId?: string): Promise { + protected deleteLocalEvent(eventId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { siteId = site.getId(); @@ -833,7 +932,7 @@ export class AddonCalendarProvider { if (timeEnd <= new Date().getTime()) { // The event has finished already, don't schedule it. - return this.deleteEvent(event.id, siteId); + return this.deleteLocalEvent(event.id, siteId); } return this.getEventReminders(event.id, siteId).then((reminders) => { diff --git a/src/addon/mod/chat/pages/chat/chat.ts b/src/addon/mod/chat/pages/chat/chat.ts index 99bcbec9416..41bd0e1cafc 100644 --- a/src/addon/mod/chat/pages/chat/chat.ts +++ b/src/addon/mod/chat/pages/chat/chat.ts @@ -65,7 +65,7 @@ export class AddonModChatChatPage { this.logger = logger.getInstance('AddonModChoiceChoicePage'); this.currentUserBeep = 'beep ' + sitesProvider.getCurrentSiteUserId(); this.isOnline = this.appProvider.isOnline(); - this.onlineObserver = network.onchange().subscribe((online) => { + this.onlineObserver = network.onchange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { this.isOnline = this.appProvider.isOnline(); diff --git a/src/addon/mod/chat/pages/users/users.ts b/src/addon/mod/chat/pages/users/users.ts index a9f4f175a05..90e59df6d94 100644 --- a/src/addon/mod/chat/pages/users/users.ts +++ b/src/addon/mod/chat/pages/users/users.ts @@ -44,7 +44,7 @@ export class AddonModChatUsersPage { this.sessionId = navParams.get('sessionId'); this.isOnline = this.appProvider.isOnline(); this.currentUserId = this.sitesProvider.getCurrentSiteUserId(); - this.onlineObserver = network.onchange().subscribe((online) => { + this.onlineObserver = network.onchange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { this.isOnline = this.appProvider.isOnline(); diff --git a/src/addon/mod/feedback/pages/form/form.ts b/src/addon/mod/feedback/pages/form/form.ts index 65a21bec8ac..f1da26ca8b4 100644 --- a/src/addon/mod/feedback/pages/form/form.ts +++ b/src/addon/mod/feedback/pages/form/form.ts @@ -82,10 +82,10 @@ export class AddonModFeedbackFormPage implements OnDestroy { this.currentSite = sitesProvider.getCurrentSite(); // Refresh online status when changes. - this.onlineObserver = network.onchange().subscribe((online) => { + this.onlineObserver = network.onchange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { - this.offline = !online; + this.offline = !this.appProvider.isOnline(); }); }); } diff --git a/src/addon/mod/forum/pages/discussion/discussion.ts b/src/addon/mod/forum/pages/discussion/discussion.ts index 68598a18ee1..7c395fc576b 100644 --- a/src/addon/mod/forum/pages/discussion/discussion.ts +++ b/src/addon/mod/forum/pages/discussion/discussion.ts @@ -115,7 +115,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { this.postId = navParams.get('postId'); this.isOnline = this.appProvider.isOnline(); - this.onlineObserver = network.onchange().subscribe((online) => { + this.onlineObserver = network.onchange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { this.isOnline = this.appProvider.isOnline(); diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 63974e50335..19cb0008db2 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -87,13 +87,19 @@ "addon.calendar.calendarevent": "Calendar event", "addon.calendar.calendarevents": "Calendar events", "addon.calendar.calendarreminders": "Calendar reminders", + "addon.calendar.confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?", + "addon.calendar.confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?", "addon.calendar.defaultnotificationtime": "Default notification time", + "addon.calendar.deleteallevents": "Delete all events", + "addon.calendar.deleteevent": "Delete event", + "addon.calendar.deleteoneevent": "Delete this event", "addon.calendar.durationminutes": "Duration in minutes", "addon.calendar.durationnone": "Without duration", "addon.calendar.durationuntil": "Until", "addon.calendar.editevent": "Editing event", "addon.calendar.errorloadevent": "Error loading event.", "addon.calendar.errorloadevents": "Error loading events.", + "addon.calendar.eventcalendareventdeleted": "Calendar event deleted", "addon.calendar.eventduration": "Duration", "addon.calendar.eventendtime": "End time", "addon.calendar.eventkind": "Type of event", diff --git a/src/core/course/classes/main-activity-component.ts b/src/core/course/classes/main-activity-component.ts index 925f9109bee..752a140e2aa 100644 --- a/src/core/course/classes/main-activity-component.ts +++ b/src/core/course/classes/main-activity-component.ts @@ -63,10 +63,10 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR const zone = injector.get(NgZone); // Refresh online status when changes. - this.onlineObserver = network.onchange().subscribe((online) => { + this.onlineObserver = network.onchange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { - this.isOnline = online; + this.isOnline = this.appProvider.isOnline(); }); }); } diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index 4f4fdab17ca..9cbfb77f5b1 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -1344,9 +1344,12 @@ export class CoreDomUtilsProvider { * @param {boolean} [needsTranslate] Whether the 'text' needs to be translated. * @param {number} [duration=2000] Duration in ms of the dimissable toast. * @param {string} [cssClass=""] Class to add to the toast. + * @param {boolean} [dismissOnPageChange=true] Dismiss the Toast on page change. * @return {Toast} Toast instance. */ - showToast(text: string, needsTranslate?: boolean, duration: number = 2000, cssClass: string = ''): Toast { + showToast(text: string, needsTranslate?: boolean, duration: number = 2000, cssClass: string = '', + dismissOnPageChange: boolean = true): Toast { + if (needsTranslate) { text = this.translate.instant(text); } @@ -1356,7 +1359,7 @@ export class CoreDomUtilsProvider { duration: duration, position: 'bottom', cssClass: cssClass, - dismissOnPageChange: true + dismissOnPageChange: dismissOnPageChange }); loader.present(); From a569bb24b81429564b49428fea847e4aaa459a8e Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 17 Jul 2019 14:49:21 +0200 Subject: [PATCH 065/241] MOBILE-3096 qbehaviour: No behaviour buttons if prevent submit --- src/core/question/providers/helper.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/question/providers/helper.ts b/src/core/question/providers/helper.ts index 01f85c86e79..e564a97d804 100644 --- a/src/core/question/providers/helper.ts +++ b/src/core/question/providers/helper.ts @@ -67,6 +67,11 @@ export class CoreQuestionHelperProvider { * @param {string} [selector] Selector to search the buttons. By default, '.im-controls input[type="submit"]'. */ extractQbehaviourButtons(question: any, selector?: string): void { + if (this.questionDelegate.getPreventSubmitMessage(question)) { + // The question is not fully supported, don't extract the buttons. + return; + } + selector = selector || '.im-controls input[type="submit"]'; const element = this.domUtils.convertToElement(question.html); @@ -76,8 +81,6 @@ export class CoreQuestionHelperProvider { buttons.forEach((button) => { this.addBehaviourButton(question, button); }); - - question.html = element.innerHTML; } /** From f2ae7b394fd8ea3c6fa2910d5b127c8c5631c230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 28 Jun 2019 13:51:50 +0200 Subject: [PATCH 066/241] MOBILE-3077 travis: Downgrade electron version to latest in v4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fc78bb983e8..82773693b44 100644 --- a/package.json +++ b/package.json @@ -210,7 +210,7 @@ } ], "compression": "maximum", - "electronVersion": "5.0.4", + "electronVersion": "4.2.5", "mac": { "category": "public.app-category.education", "icon": "resources/desktop/icon.icns", From 8766f611b5de44d5c2e29facb787ab9ccd615517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 17 Jul 2019 16:01:00 +0200 Subject: [PATCH 067/241] MOBILE-3077 ionic: Add bundle version for OSX --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 82773693b44..3c1c766ce13 100644 --- a/package.json +++ b/package.json @@ -215,6 +215,7 @@ "category": "public.app-category.education", "icon": "resources/desktop/icon.icns", "target": "mas", + "bundleVersion": "3.7.0", "extendInfo": { "ElectronTeamID": "2NU57U5PAW" } From 38ccc712c10a9d3528b94a3ccaecc91728f75fa7 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 17 Jul 2019 16:17:15 +0200 Subject: [PATCH 068/241] MOBILE-3079 core: Handle split view in user profile directives --- .../competency/pages/competency/competency.html | 2 +- .../competency/pages/competency/competency.ts | 11 ----------- .../submission/addon-mod-assign-submission.html | 6 +++--- .../assign/components/submission/submission.ts | 11 ----------- .../components/post/addon-mod-forum-post.html | 2 +- src/addon/mod/forum/components/post/post.ts | 16 +--------------- src/components/user-avatar/user-avatar.ts | 17 +++++++++++++---- src/directives/user-link.ts | 11 +++++++++-- 8 files changed, 28 insertions(+), 48 deletions(-) diff --git a/src/addon/competency/pages/competency/competency.html b/src/addon/competency/pages/competency/competency.html index dc048e44659..4f66d9dc1c4 100644 --- a/src/addon/competency/pages/competency/competency.html +++ b/src/addon/competency/pages/competency/competency.html @@ -76,7 +76,7 @@

{{ 'addon.competency.evidence' | translate }}

{{ 'addon.competency.noevidence' | translate }}

- +

{{ evidence.actionuser.fullname }}

{{ evidence.timemodified * 1000 | coreFormatDate }}

diff --git a/src/addon/competency/pages/competency/competency.ts b/src/addon/competency/pages/competency/competency.ts index dc7e76c9d16..c3930b9452b 100644 --- a/src/addon/competency/pages/competency/competency.ts +++ b/src/addon/competency/pages/competency/competency.ts @@ -151,15 +151,4 @@ export class AddonCompetencyCompetencyPage { const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; navCtrl.push('AddonCompetencyCompetencySummaryPage', {competencyId}); } - - /** - * Opens the profile of a user. - * - * @param {number} userId - */ - openUserProfile(userId: number): void { - // Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav. - const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; - navCtrl.push('CoreUserProfilePage', {userId, courseId: this.courseId}); - } } diff --git a/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html b/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html index 0ff33520222..c46b8942ece 100644 --- a/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html +++ b/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html @@ -1,7 +1,7 @@ -
+

{{ user.fullname }}

@@ -110,7 +110,7 @@

{{ 'addon.mod_assign.attemptnumber' | translate }}

{{ 'addon.mod_assign.userswhoneedtosubmit' | translate: {$a: ''} }}

- +

{{ user.fullname }}

@@ -208,7 +208,7 @@

{{ 'addon.mod_assign.attemptsettings' | translate }}

- +

{{ 'addon.mod_assign.gradedby' | translate }}

{{ grader.fullname }}

diff --git a/src/addon/mod/assign/components/submission/submission.ts b/src/addon/mod/assign/components/submission/submission.ts index c3f655855e4..9d5fd32d2c2 100644 --- a/src/addon/mod/assign/components/submission/submission.ts +++ b/src/addon/mod/assign/components/submission/submission.ts @@ -620,17 +620,6 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { }); } - /** - * Open a user profile. - * - * @param {number} userId User to open. - */ - openUserProfile(userId: number): void { - // Open a user profile. If this component is inside a split view, use the master nav to open it. - const navCtrl = this.splitviewCtrl ? this.splitviewCtrl.getMasterNav() : this.navCtrl; - navCtrl.push('CoreUserProfilePage', { userId: userId, courseId: this.courseId }); - } - /** * Set the submission status name and class. * diff --git a/src/addon/mod/forum/components/post/addon-mod-forum-post.html b/src/addon/mod/forum/components/post/addon-mod-forum-post.html index cb8ffc1b328..9119c2beda8 100644 --- a/src/addon/mod/forum/components/post/addon-mod-forum-post.html +++ b/src/addon/mod/forum/components/post/addon-mod-forum-post.html @@ -1,6 +1,6 @@ - +

diff --git a/src/addon/mod/forum/components/post/post.ts b/src/addon/mod/forum/components/post/post.ts index 581c7eefe1c..a8d010a9eb0 100644 --- a/src/addon/mod/forum/components/post/post.ts +++ b/src/addon/mod/forum/components/post/post.ts @@ -14,10 +14,9 @@ import { Component, Input, Output, Optional, EventEmitter, OnInit, OnDestroy } from '@angular/core'; import { FormControl } from '@angular/forms'; -import { NavController, Content } from 'ionic-angular'; +import { Content } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; -import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreSyncProvider } from '@providers/sync'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -57,7 +56,6 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy { protected syncId: string; constructor( - private navCtrl: NavController, private uploaderProvider: CoreFileUploaderProvider, private syncProvider: CoreSyncProvider, private domUtils: CoreDomUtilsProvider, @@ -67,7 +65,6 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy { private forumHelper: AddonModForumHelperProvider, private forumOffline: AddonModForumOfflineProvider, private forumSync: AddonModForumSyncProvider, - @Optional() private svComponent: CoreSplitViewComponent, @Optional() private content: Content) { this.onPostChange = new EventEmitter(); } @@ -79,17 +76,6 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy { this.uniqueId = this.post.id ? 'reply' + this.post.id : 'edit' + this.post.parent; } - /** - * Opens the profile of a user. - * - * @param {number} userId - */ - openUserProfile(userId: number): void { - // Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav. - const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; - navCtrl.push('CoreUserProfilePage', {userId, courseId: this.courseId}); - } - /** * Set data to new post, clearing temporary files and updating original data. * diff --git a/src/components/user-avatar/user-avatar.ts b/src/components/user-avatar/user-avatar.ts index 298ace24afb..22c6fe5a286 100644 --- a/src/components/user-avatar/user-avatar.ts +++ b/src/components/user-avatar/user-avatar.ts @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange } from '@angular/core'; +import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Optional } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreSitesProvider } from '@providers/sites'; import { CoreAppProvider } from '@providers/app'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreEventsProvider } from '@providers/events'; import { CoreUserProvider } from '@core/user/providers/user'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; /** * Component to display a "user avatar". @@ -48,8 +49,13 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy { protected currentUserId: number; protected pictureObs; - constructor(private navCtrl: NavController, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, - private appProvider: CoreAppProvider, eventsProvider: CoreEventsProvider) { + constructor(private navCtrl: NavController, + private sitesProvider: CoreSitesProvider, + private utils: CoreUtilsProvider, + private appProvider: CoreAppProvider, + eventsProvider: CoreEventsProvider, + @Optional() private svComponent: CoreSplitViewComponent) { + this.currentUserId = this.sitesProvider.getCurrentSiteUserId(); this.pictureObs = eventsProvider.on(CoreUserProvider.PROFILE_PICTURE_UPDATED, (data) => { @@ -121,7 +127,10 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy { if (this.linkProfile && this.userId) { event.preventDefault(); event.stopPropagation(); - this.navCtrl.push('CoreUserProfilePage', { userId: this.userId, courseId: this.courseId }); + + // Decide which navCtrl to use. If this component is inside a split view, use the split view's master nav. + const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; + navCtrl.push('CoreUserProfilePage', { userId: this.userId, courseId: this.courseId }); } } diff --git a/src/directives/user-link.ts b/src/directives/user-link.ts index d1714bacb50..981e489b1f9 100644 --- a/src/directives/user-link.ts +++ b/src/directives/user-link.ts @@ -14,6 +14,7 @@ import { Directive, Input, OnInit, ElementRef, Optional } from '@angular/core'; import { NavController } from 'ionic-angular'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; /** * Directive to go to user profile on click. @@ -27,7 +28,10 @@ export class CoreUserLinkDirective implements OnInit { protected element: HTMLElement; - constructor(element: ElementRef, @Optional() private navCtrl: NavController) { + constructor(element: ElementRef, + @Optional() private navCtrl: NavController, + @Optional() private svComponent: CoreSplitViewComponent) { + // This directive can be added dynamically. In that case, the first param is the anchor HTMLElement. this.element = element.nativeElement || element; } @@ -41,7 +45,10 @@ export class CoreUserLinkDirective implements OnInit { if (!event.defaultPrevented) { event.preventDefault(); event.stopPropagation(); - this.navCtrl.push('CoreUserProfilePage', { userId: this.userId, courseId: this.courseId }); + + // Decide which navCtrl to use. If this directive is inside a split view, use the split view's master nav. + const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; + navCtrl.push('CoreUserProfilePage', { userId: this.userId, courseId: this.courseId }); } }); } From fa837532d4557ef9f3c2a4886be1419fa6a850ca Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 1 Jul 2019 10:24:14 +0200 Subject: [PATCH 069/241] MOBILE-3021 calendar: Go to new page if monthly view supported --- src/addon/calendar/calendar.module.ts | 8 +- src/addon/calendar/pages/index/index.html | 28 +++ .../calendar/pages/index/index.module.ts | 35 +++ src/addon/calendar/pages/index/index.ts | 38 ++++ src/addon/calendar/providers/calendar.ts | 202 ++++++++++++++++++ .../calendar/providers/mainmenu-handler.ts | 2 +- 6 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 src/addon/calendar/pages/index/index.html create mode 100644 src/addon/calendar/pages/index/index.module.ts create mode 100644 src/addon/calendar/pages/index/index.ts diff --git a/src/addon/calendar/calendar.module.ts b/src/addon/calendar/calendar.module.ts index c8f9cef8209..66ab9f23717 100644 --- a/src/addon/calendar/calendar.module.ts +++ b/src/addon/calendar/calendar.module.ts @@ -70,7 +70,13 @@ export class AddonCalendarModule { return; } - loginHelper.redirect('AddonCalendarListPage', {eventId: data.eventid}, data.siteId); + // Check which page we should load. + calendarProvider.canViewMonth(data.siteId).then((canView) => { + const pageName = canView ? 'AddonCalendarIndexPage' : 'AddonCalendarListPage'; + + loginHelper.redirect(pageName, {eventId: data.eventid}, data.siteId); + }); + }); }); } diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html new file mode 100644 index 00000000000..122436442d6 --- /dev/null +++ b/src/addon/calendar/pages/index/index.html @@ -0,0 +1,28 @@ + + + {{ 'addon.calendar.calendarevents' | translate }} + + + + + + + + + + + + + + + + + + + + + diff --git a/src/addon/calendar/pages/index/index.module.ts b/src/addon/calendar/pages/index/index.module.ts new file mode 100644 index 00000000000..0c3486dd9a2 --- /dev/null +++ b/src/addon/calendar/pages/index/index.module.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonCalendarIndexPage } from './index'; + +@NgModule({ + declarations: [ + AddonCalendarIndexPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(AddonCalendarIndexPage), + TranslateModule.forChild() + ], +}) +export class AddonCalendarIndexPageModule {} diff --git a/src/addon/calendar/pages/index/index.ts b/src/addon/calendar/pages/index/index.ts new file mode 100644 index 00000000000..3c1be5ed2ad --- /dev/null +++ b/src/addon/calendar/pages/index/index.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, OnInit } from '@angular/core'; +import { IonicPage } from 'ionic-angular'; + +/** + * Page that displays the calendar events. + */ +@IonicPage({ segment: 'addon-calendar-index' }) +@Component({ + selector: 'page-addon-calendar-index', + templateUrl: 'index.html', +}) +export class AddonCalendarIndexPage implements OnInit { + + constructor() { + // @todo + } + + /** + * View loaded. + */ + ngOnInit(): void { + // @todo + } +} diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 9e34520a190..fdc4905d365 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -237,6 +237,8 @@ export class AddonCalendarProvider { canDeleteEvents(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return this.canDeleteEventsInSite(site); + }).catch(() => { + return false; }); } @@ -263,6 +265,8 @@ export class AddonCalendarProvider { canEditEvents(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return this.canEditEventsInSite(site); + }).catch(() => { + return false; }); } @@ -280,6 +284,34 @@ export class AddonCalendarProvider { return site.isVersionGreaterEqualThan('3.7.1'); } + /** + * Check if a certain site allows viewing events in monthly view. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if monthly view is supported. + * @since 3.4 + */ + canViewMonth(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.canViewMonthInSite(site); + }).catch(() => { + return false; + }); + } + + /** + * Check if a certain site allows viewing events in monthly view. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether monthly view is supported. + * @since 3.4 + */ + canViewMonthInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.wsAvailable('core_calendar_get_calendar_monthly_view'); + } + /** * Removes expired events from local DB. * @@ -723,6 +755,126 @@ export class AddonCalendarProvider { return this.getEventsListPrefixCacheKey() + daysToStart + ':' + daysInterval; } + /** + * Get monthly calendar events. + * + * @param {number} year Year to get. + * @param {number} month Month to get. + * @param {number} [courseId] Course to get. + * @param {number} [categoryId] Category to get. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the response. + */ + getMonthlyEvents(year: number, month: number, courseId?: number, categoryId?: number, siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + + const data: any = { + year: year, + month: month, + mini: 1 // Set mini to 1 to prevent returning the course selector HTML. + }; + + if (courseId) { + data.courseid = courseId; + } + if (categoryId) { + data.categoryid = categoryId; + } + + const preSets = { + cacheKey: this.getMonthlyEventsCacheKey(year, month, courseId, categoryId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES + }; + + return site.read('core_calendar_get_calendar_monthly_view', data, preSets); + }); + } + + /** + * Get prefix cache key for monthly events WS calls. + * + * @return {string} Prefix Cache key. + */ + protected getMonthlyEventsPrefixCacheKey(): string { + return this.ROOT_CACHE_KEY + 'monthly:'; + } + + /** + * Get prefix cache key for a certain month for monthly events WS calls. + * + * @param {number} year Year to get. + * @param {number} month Month to get. + * @return {string} Prefix Cache key. + */ + protected getMonthlyEventsMonthPrefixCacheKey(year: number, month: number): string { + return this.getMonthlyEventsPrefixCacheKey() + year + ':' + month + ':'; + } + + /** + * Get cache key for monthly events WS calls. + * + * @param {number} year Year to get. + * @param {number} month Month to get. + * @param {number} [courseId] Course to get. + * @param {number} [categoryId] Category to get. + * @return {string} Cache key. + */ + protected getMonthlyEventsCacheKey(year: number, month: number, courseId?: number, categoryId?: number): string { + return this.getMonthlyEventsMonthPrefixCacheKey(year, month) + (courseId ? courseId : '') + ':' + + (categoryId ? categoryId : ''); + } + + /** + * Get upcoming calendar events. + * + * @param {number} [courseId] Course to get. + * @param {number} [categoryId] Category to get. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the response. + */ + getUpcomingEvents(courseId?: number, categoryId?: number, siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + + const data: any = {}; + + if (courseId) { + data.courseid = courseId; + } + if (categoryId) { + data.categoryid = categoryId; + } + + const preSets = { + cacheKey: this.getUpcomingEventsCacheKey(courseId, categoryId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES + }; + + return site.read('core_calendar_get_calendar_upcoming_view', data, preSets); + }); + } + + /** + * Get prefix cache key for upcoming events WS calls. + * + * @return {string} Prefix Cache key. + */ + protected getUpcomingEventsPrefixCacheKey(): string { + return this.ROOT_CACHE_KEY + 'upcoming:'; + } + + /** + * Get cache key for upcoming events WS calls. + * + * @param {number} [courseId] Course to get. + * @param {number} [categoryId] Category to get. + * @return {string} Cache key. + */ + protected getUpcomingEventsCacheKey(courseId?: number, categoryId?: number): string { + return this.getUpcomingEventsPrefixCacheKey() + (courseId ? courseId : '') + ':' + (categoryId ? categoryId : ''); + } + /** * Invalidates access information. * @@ -782,6 +934,56 @@ export class AddonCalendarProvider { }); } + /** + * Invalidates monthly events for all months. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAllMonthlyEvents(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getMonthlyEventsPrefixCacheKey()); + }); + } + + /** + * Invalidates monthly events for a certain months. + * + * @param {number} year Year. + * @param {number} month Month. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateMonthlyEvents(year: number, month: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getMonthlyEventsMonthPrefixCacheKey(year, month)); + }); + } + + /** + * Invalidates upcoming events for all courses and categories. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAllUpcomingEvents(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getUpcomingEventsPrefixCacheKey()); + }); + } + + /** + * Invalidates upcoming events for a certain course or category. + * + * @param {number} [courseId] Course ID. + * @param {number} [categoryId] Category ID. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateUpcomingEvents(courseId?: number, categoryId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getUpcomingEventsCacheKey(courseId, categoryId)); + }); + } + /** * Check if Calendar is disabled in a certain site. * diff --git a/src/addon/calendar/providers/mainmenu-handler.ts b/src/addon/calendar/providers/mainmenu-handler.ts index 2769f0e0bfb..0568eeb01ea 100644 --- a/src/addon/calendar/providers/mainmenu-handler.ts +++ b/src/addon/calendar/providers/mainmenu-handler.ts @@ -44,7 +44,7 @@ export class AddonCalendarMainMenuHandler implements CoreMainMenuHandler { return { icon: 'calendar', title: 'addon.calendar.calendar', - page: 'AddonCalendarListPage', + page: this.calendarProvider.canViewMonthInSite() ? 'AddonCalendarIndexPage' : 'AddonCalendarListPage', class: 'addon-calendar-handler' }; } From 9e107cf667c2152c89a1d738010df7ba5ccc7453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 18 Jul 2019 12:28:58 +0200 Subject: [PATCH 070/241] MOBILE-3076 splash: Fix splash path --- src/core/login/pages/init/init.scss | 2 +- src/theme/variables.scss | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/login/pages/init/init.scss b/src/core/login/pages/init/init.scss index f456779df55..f50f047fe3a 100644 --- a/src/core/login/pages/init/init.scss +++ b/src/core/login/pages/init/init.scss @@ -17,7 +17,7 @@ ion-app.app-root page-core-login-init { text-align: center; vertical-align: middle; - background-image: url('/assets/img/splash.png'); + background-image: url("#{$assets-path}/img/splash.png"); background-repeat: no-repeat; background-size: 100%; background-size: $core-splash-bgsize; diff --git a/src/theme/variables.scss b/src/theme/variables.scss index 919afa355da..44839421489 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -4,6 +4,7 @@ // Font path is used to include ionicons, // roboto, and noto sans fonts $font-path: "../assets/fonts"; +$assets-path: "../assets"; // The app direction is used to include // rtl styles in your app. For more info, please see: From 661709acb47339b5dc11c6eb7c794b73ed62167f Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 3 Jul 2019 08:13:02 +0200 Subject: [PATCH 071/241] MOBILE-3021 calendar: Support monthly view --- scripts/langindex.json | 15 ++ .../calendar/addon-calendar-calendar.html | 52 +++++ .../components/calendar/calendar.scss | 42 ++++ .../calendar/components/calendar/calendar.ts | 221 ++++++++++++++++++ .../calendar/components/components.module.ts | 40 ++++ src/addon/calendar/lang/en.json | 16 +- src/addon/calendar/pages/index/index.html | 3 +- .../calendar/pages/index/index.module.ts | 2 + src/addon/calendar/pages/index/index.ts | 159 ++++++++++++- src/addon/calendar/pages/list/list.ts | 48 +--- src/addon/calendar/providers/calendar.ts | 43 ++++ src/addon/calendar/providers/helper.ts | 74 +++++- src/assets/lang/en.json | 15 ++ src/lang/en.json | 1 + src/theme/variables.scss | 1 + 15 files changed, 678 insertions(+), 54 deletions(-) create mode 100644 src/addon/calendar/components/calendar/addon-calendar-calendar.html create mode 100644 src/addon/calendar/components/calendar/calendar.scss create mode 100644 src/addon/calendar/components/calendar/calendar.ts create mode 100644 src/addon/calendar/components/components.module.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 40cfc905d7d..2f79cd0a08b 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -106,9 +106,13 @@ "addon.calendar.eventname": "calendar", "addon.calendar.eventstarttime": "calendar", "addon.calendar.eventtype": "calendar", + "addon.calendar.fri": "calendar", + "addon.calendar.friday": "calendar", "addon.calendar.gotoactivity": "calendar", "addon.calendar.invalidtimedurationminutes": "calendar", "addon.calendar.invalidtimedurationuntil": "calendar", + "addon.calendar.mon": "calendar", + "addon.calendar.monday": "calendar", "addon.calendar.newevent": "calendar", "addon.calendar.noevents": "local_moodlemobileapp", "addon.calendar.nopermissiontoupdatecalendar": "error", @@ -118,7 +122,15 @@ "addon.calendar.repeateditthis": "calendar", "addon.calendar.repeatevent": "calendar", "addon.calendar.repeatweeksl": "calendar", + "addon.calendar.sat": "calendar", + "addon.calendar.saturday": "calendar", "addon.calendar.setnewreminder": "local_moodlemobileapp", + "addon.calendar.sun": "calendar", + "addon.calendar.sunday": "calendar", + "addon.calendar.thu": "calendar", + "addon.calendar.thursday": "calendar", + "addon.calendar.tue": "calendar", + "addon.calendar.tuesday": "calendar", "addon.calendar.typecategory": "calendar", "addon.calendar.typeclose": "calendar", "addon.calendar.typecourse": "calendar", @@ -128,6 +140,8 @@ "addon.calendar.typeopen": "calendar", "addon.calendar.typesite": "calendar", "addon.calendar.typeuser": "calendar", + "addon.calendar.wed": "calendar", + "addon.calendar.wednesday": "calendar", "addon.competency.activities": "tool_lp", "addon.competency.competencies": "competency", "addon.competency.competenciesmostoftennotproficientincourse": "tool_lp", @@ -1657,6 +1671,7 @@ "core.notingroup": "moodle", "core.notsent": "local_moodlemobileapp", "core.now": "moodle", + "core.nummore": "local_moodlemobileapp", "core.numwords": "moodle", "core.offline": "message", "core.ok": "moodle", diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html new file mode 100644 index 00000000000..04d35dfda85 --- /dev/null +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -0,0 +1,52 @@ + + + + + + + + + + +

{{ periodName }}

+
+ + + + + + + + + + + + + +

{{ day.shortname | translate }}

+
+
+ + + + + +

{{ day.mday }}

+ + +

+ + +
+

+ + {{event.name}} +

+

{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}

+
+
+ +
+
+ + diff --git a/src/addon/calendar/components/calendar/calendar.scss b/src/addon/calendar/components/calendar/calendar.scss new file mode 100644 index 00000000000..853cff23a49 --- /dev/null +++ b/src/addon/calendar/components/calendar/calendar.scss @@ -0,0 +1,42 @@ + +$calendar-event-category-color: $purple !default; // Purple. +$calendar-event-course-color: $red !default; // Red. +$calendar-event-group-color: $yellow !default; // Yellow. +$calendar-event-user-color: $blue !default; // Blue. +$calendar-event-site-color: $green !default; // Green. + +ion-app.app-root addon-calendar-calendar { + + .addon-calendar-weekdays { + opacity: 0.4; + } + + .addon-calendar-day-events { + @include text-align('start'); + } + + .calendar_event_type { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + border: 1px solid white; + @include margin-horizontal(1px, 1px); + + &.calendar_event_category { + background-color: $calendar-event-category-color; + } + &.calendar_event_course { + background-color: $calendar-event-course-color; + } + &.calendar_event_group { + background-color: $calendar-event-group-color; + } + &.calendar_event_user { + background-color: $calendar-event-user-color; + } + &.calendar_event_site { + background-color: $calendar-event-site-color; + } + } +} \ No newline at end of file diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts new file mode 100644 index 00000000000..9bb41721ece --- /dev/null +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -0,0 +1,221 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, OnDestroy, OnInit, Input, OnChanges, SimpleChange } from '@angular/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; + +/** + * Component that displays a calendar. + */ +@Component({ + selector: 'addon-calendar-calendar', + templateUrl: 'addon-calendar-calendar.html', +}) +export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDestroy { + @Input() initialYear: number | string; // Initial year to load. + @Input() initialMonth: number | string; // Initial month to load. + @Input() courseId: number | string; + @Input() categoryId: number | string; // Category ID the course belongs to. + @Input() canNavigate?: string | boolean; // Whether to include arrows to change the month. Defaults to true. + + periodName: string; + weekDays: any[]; + weeks: any[]; + loaded = false; + + protected year: number; + protected month: number; + protected categoriesRetrieved = false; + protected categories = {}; + + constructor(eventsProvider: CoreEventsProvider, + sitesProvider: CoreSitesProvider, + private calendarProvider: AddonCalendarProvider, + private calendarHelper: AddonCalendarHelperProvider, + private domUtils: CoreDomUtilsProvider, + private timeUtils: CoreTimeUtilsProvider, + private utils: CoreUtilsProvider, + private coursesProvider: CoreCoursesProvider) { + + } + + /** + * Component loaded. + */ + ngOnInit(): void { + const now = new Date(); + + this.year = this.initialYear ? Number(this.initialYear) : now.getFullYear(); + this.month = this.initialYear ? Number(this.initialYear) : now.getMonth() + 1; + this.canNavigate = typeof this.canNavigate == 'undefined' ? true : this.utils.isTrueOrOne(this.canNavigate); + + this.fetchData(); + } + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + + if ((changes.courseId || changes.categoryId) && this.weeks) { + const courseId = this.courseId ? Number(this.courseId) : undefined, + categoryId = this.categoryId ? Number(this.categoryId) : undefined; + + this.filterEvents(courseId, categoryId); + } + } + + /** + * Fetch contacts. + * + * @param {boolean} [refresh=false] True if we are refreshing contacts, false if we are loading more. + * @return {Promise} Promise resolved when done. + */ + fetchData(refresh: boolean = false): Promise { + const courseId = this.courseId ? Number(this.courseId) : undefined, + categoryId = this.categoryId ? Number(this.categoryId) : undefined, + promises = []; + + promises.push(this.loadCategories()); + + promises.push(this.calendarProvider.getMonthlyEvents(this.year, this.month, courseId, categoryId).then((result) => { + + // Calculate the period name. We don't use the one in result because it's in server's language. + this.periodName = this.timeUtils.userDate(new Date(this.year, this.month - 1).getTime(), 'core.strftimemonthyear'); + + this.weekDays = this.calendarProvider.getWeekDays(result.daynames[0].dayno); + this.weeks = result.weeks; + + this.filterEvents(courseId, categoryId); + })); + + return Promise.all(promises).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Load categories to be able to filter events. + * + * @return {Promise} Promise resolved when done. + */ + protected loadCategories(): Promise { + if (this.categoriesRetrieved) { + // Already retrieved, stop. + return Promise.resolve(); + } + + return this.coursesProvider.getCategories(0, true).then((cats) => { + this.categoriesRetrieved = true; + this.categories = {}; + + // Index categories by ID. + cats.forEach((category) => { + this.categories[category.id] = category; + }); + }).catch(() => { + // Ignore errors. + }); + } + + /** + * Filter events to only display events belonging to a certain course. + * + * @param {number} courseId Course ID. + * @param {number} categoryId Category the course belongs to. + */ + filterEvents(courseId: number, categoryId: number): void { + + this.weeks.forEach((week) => { + week.days.forEach((day) => { + if (!courseId || courseId < 0) { + day.filteredEvents = day.events; + } else { + day.filteredEvents = day.events.filter((event) => { + return this.calendarHelper.shouldDisplayEvent(event, courseId, categoryId, this.categories); + }); + } + + // Re-calculate some properties. + this.calendarHelper.calculateDayData(day, day.filteredEvents); + }); + }); + } + + /** + * Refresh events. + * + * @return {Promise} Promise resolved when done. + */ + refreshData(): Promise { + const promises = []; + + promises.push(this.calendarProvider.invalidateMonthlyEvents(this.year, this.month)); + promises.push(this.coursesProvider.invalidateCategories(0, true)); + + this.categoriesRetrieved = false; // Get categories again. + + return Promise.all(promises).then(() => { + return this.fetchData(true); + }); + } + + /** + * Load next month. + */ + loadNext(): void { + if (this.month === 12) { + this.month = 1; + this.year++; + } else { + this.month++; + } + + this.loaded = false; + + this.fetchData(); + } + + /** + * Load previous month. + */ + loadPrevious(): void { + if (this.month === 1) { + this.month = 12; + this.year--; + } else { + this.month--; + } + + this.loaded = false; + + this.fetchData(); + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + // @todo + } +} diff --git a/src/addon/calendar/components/components.module.ts b/src/addon/calendar/components/components.module.ts new file mode 100644 index 00000000000..a6d5afe2254 --- /dev/null +++ b/src/addon/calendar/components/components.module.ts @@ -0,0 +1,40 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonCalendarCalendarComponent } from '../components/calendar/calendar'; + +@NgModule({ + declarations: [ + AddonCalendarCalendarComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + ], + exports: [ + AddonCalendarCalendarComponent + ] +}) +export class AddonCalendarComponentsModule {} diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index 8792b48c52a..412c8174240 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -22,9 +22,13 @@ "eventname": "Event title", "eventstarttime": "Start time", "eventtype": "Event type", + "fri": "Fri", + "friday": "Friday", "gotoactivity": "Go to activity", "invalidtimedurationminutes": "The duration in minutes you have entered is invalid. Please enter the duration in minutes greater than 0 or select no duration.", "invalidtimedurationuntil": "The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.", + "mon": "Mon", + "monday": "Monday", "newevent": "New event", "noevents": "There are no events", "nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event", @@ -34,7 +38,15 @@ "repeateditthis": "Apply changes to this event only", "repeatevent": "Repeat this event", "repeatweeksl": "Repeat weekly, creating altogether", + "sat": "Sat", + "saturday": "Saturday", "setnewreminder": "Set a new reminder", + "sun": "Sun", + "sunday": "Sunday", + "thu": "Thu", + "thursday": "Thursday", + "tue": "Tue", + "tuesday": "Tuesday", "typeclose": "Close event", "typecourse": "Course event", "typecategory": "Category event", @@ -43,5 +55,7 @@ "typegroup": "Group event", "typeopen": "Open event", "typesite": "Site event", - "typeuser": "User event" + "typeuser": "User event", + "wed": "Wed", + "wednesday": "Wednesday" } \ No newline at end of file diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html index 122436442d6..42c3cd4b927 100644 --- a/src/addon/calendar/pages/index/index.html +++ b/src/addon/calendar/pages/index/index.html @@ -15,9 +15,8 @@ - - + diff --git a/src/addon/calendar/pages/index/index.module.ts b/src/addon/calendar/pages/index/index.module.ts index 0c3486dd9a2..bf925e79931 100644 --- a/src/addon/calendar/pages/index/index.module.ts +++ b/src/addon/calendar/pages/index/index.module.ts @@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonCalendarComponentsModule } from '../../components/components.module'; import { AddonCalendarIndexPage } from './index'; @NgModule({ @@ -28,6 +29,7 @@ import { AddonCalendarIndexPage } from './index'; CoreComponentsModule, CoreDirectivesModule, CorePipesModule, + AddonCalendarComponentsModule, IonicPageModule.forChild(AddonCalendarIndexPage), TranslateModule.forChild() ], diff --git a/src/addon/calendar/pages/index/index.ts b/src/addon/calendar/pages/index/index.ts index 3c1be5ed2ad..9ab19cd7e3a 100644 --- a/src/addon/calendar/pages/index/index.ts +++ b/src/addon/calendar/pages/index/index.ts @@ -8,12 +8,20 @@ // // 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. +// WITHOUT WARRANTIES OR CONDITIONS OFx ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit } from '@angular/core'; -import { IonicPage } from 'ionic-angular'; +import { Component, OnInit, ViewChild } from '@angular/core'; +import { IonicPage, NavParams, NavController, PopoverController } from 'ionic-angular'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { AddonCalendarCalendarComponent } from '../../components/calendar/calendar'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; +import { TranslateService } from '@ngx-translate/core'; /** * Page that displays the calendar events. @@ -24,15 +32,154 @@ import { IonicPage } from 'ionic-angular'; templateUrl: 'index.html', }) export class AddonCalendarIndexPage implements OnInit { + @ViewChild(AddonCalendarCalendarComponent) calendarComponent: AddonCalendarCalendarComponent; - constructor() { - // @todo + protected allCourses = { + id: -1, + fullname: this.translate.instant('core.fulllistofcourses'), + category: -1 + }; + + courseId: number; + categoryId: number; + canCreate = false; + courses: any[]; + notificationsEnabled = false; + loaded = false; + + constructor(localNotificationsProvider: CoreLocalNotificationsProvider, + navParams: NavParams, + private navCtrl: NavController, + private domUtils: CoreDomUtilsProvider, + private calendarProvider: AddonCalendarProvider, + private calendarHelper: AddonCalendarHelperProvider, + private translate: TranslateService, + private coursesProvider: CoreCoursesProvider, + private popoverCtrl: PopoverController) { + + this.courseId = navParams.get('courseId'); + this.notificationsEnabled = localNotificationsProvider.isAvailable(); } /** * View loaded. */ ngOnInit(): void { - // @todo + this.fetchData(); + } + + /** + * Fetch all the data required for the view. + * + * @return {Promise} Promise resolved when done. + */ + fetchData(): Promise { + const promises = []; + + // Load courses for the popover. + promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { + // Add "All courses". + courses.unshift(this.allCourses); + this.courses = courses; + + if (this.courseId) { + // Search the course to get the category. + const course = this.courses.find((course) => { + return course.id == this.courseId; + }); + + if (course) { + this.categoryId = course.category; + } + } + })); + + // Check if user can create events. + promises.push(this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { + this.canCreate = canEdit; + })); + + return Promise.all(promises).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Refresh the data. + * + * @param {any} [refresher] Refresher. + * @return {Promise} Promise resolved when done. + */ + doRefresh(refresher?: any): void { + if (!this.loaded) { + return; + } + + const promises = []; + + promises.push(this.calendarProvider.invalidateAllowedEventTypes().then(() => { + return this.fetchData(); + })); + + // Refresh the sub-component. + promises.push(this.calendarComponent.refreshData()); + + Promise.all(promises).finally(() => { + refresher && refresher.complete(); + }); + } + + /** + * Show the context menu. + * + * @param {MouseEvent} event Event. + */ + openCourseFilter(event: MouseEvent): void { + const popover = this.popoverCtrl.create(CoreCoursePickerMenuPopoverComponent, { + courses: this.courses, + courseId: this.courseId + }); + + popover.onDidDismiss((course) => { + if (course) { + this.courseId = course.id > 0 ? course.id : undefined; + this.categoryId = course.id > 0 ? course.category : undefined; + + // Course viewed has changed, check if the user can create events for this course calendar. + this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { + this.canCreate = canEdit; + }); + } + }); + popover.present({ + ev: event + }); + } + + /** + * Open page to create/edit an event. + * + * @param {number} [eventId] Event ID to edit. + */ + openEdit(eventId?: number): void { + const params: any = {}; + + if (eventId) { + params.eventId = eventId; + } + if (this.courseId) { + params.courseId = this.courseId; + } + + this.navCtrl.push('AddonCalendarEditEventPage', params); + } + + /** + * Open calendar events settings. + */ + openSettings(): void { + this.navCtrl.push('AddonCalendarSettingsPage'); } } diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index 0dac7f74b54..00c6657d422 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -423,50 +423,10 @@ export class AddonCalendarListPage implements OnDestroy { return this.events; } - return this.events.filter(this.shouldDisplayEvent.bind(this)); - } - - /** - * Check if an event should be displayed based on the filter. - * - * @param {any} event Event object. - * @return {boolean} Whether it should be displayed. - */ - protected shouldDisplayEvent(event: any): boolean { - if (event.eventtype == 'user' || event.eventtype == 'site') { - // User or site event, display it. - return true; - } - - if (event.eventtype == 'category') { - if (!event.categoryid || !Object.keys(this.categories).length) { - // We can't tell if the course belongs to the category, display them all. - return true; - } - if (event.categoryid == this.filter.course.category) { - // The event is in the same category as the course, display it. - return true; - } - - // Check parent categories. - let category = this.categories[this.filter.course.category]; - while (category) { - if (!category.parent) { - // Category doesn't have parent, stop. - break; - } - - if (event.categoryid == category.parent) { - return true; - } - category = this.categories[category.parent]; - } - - return false; - } - - // Show the event if it is from site home or if it matches the selected course. - return event.courseid === this.siteHomeId || event.courseid == this.filter.course.id; + return this.events.filter((event) => { + return this.calendarHelper.shouldDisplayEvent(event, this.filter.course.id, this.filter.course.category, + this.categories); + }); } /** diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index fdc4905d365..4705a8ad80b 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -51,6 +51,37 @@ export class AddonCalendarProvider { static TYPE_USER = 'user'; protected ROOT_CACHE_KEY = 'mmaCalendar:'; + protected weekDays = [ + { + shortname: 'addon.calendar.sun', + fullname: 'addon.calendar.sunday' + }, + { + shortname: 'addon.calendar.mon', + fullname: 'addon.calendar.monday' + }, + { + shortname: 'addon.calendar.tue', + fullname: 'addon.calendar.tuesday' + }, + { + shortname: 'addon.calendar.wed', + fullname: 'addon.calendar.wednesday' + }, + { + shortname: 'addon.calendar.thu', + fullname: 'addon.calendar.thursday' + }, + { + shortname: 'addon.calendar.fri', + fullname: 'addon.calendar.friday' + }, + { + shortname: 'addon.calendar.sat', + fullname: 'addon.calendar.saturday' + } + ]; + // Variables for database. static EVENTS_TABLE = 'addon_calendar_events_2'; static REMINDERS_TABLE = 'addon_calendar_reminders'; @@ -875,6 +906,18 @@ export class AddonCalendarProvider { return this.getUpcomingEventsPrefixCacheKey() + (courseId ? courseId : '') + ':' + (categoryId ? categoryId : ''); } + /** + * Get the week days, already ordered according to a specified starting day. + * + * @param {number} [startingDay=0] Starting day. 0=Sunday, 1=Monday, ... + * @return {any[]} Week days. + */ + getWeekDays(startingDay?: number): any[] { + startingDay = startingDay || 0; + + return this.weekDays.slice(startingDay).concat(this.weekDays.slice(0, startingDay)); + } + /** * Invalidates access information. * diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index ae857225d39..94cfa1e9386 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -14,6 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; import { CoreCourseProvider } from '@core/course/providers/course'; import { AddonCalendarProvider } from './calendar'; import { CoreConstants } from '@core/constants'; @@ -33,11 +34,35 @@ export class AddonCalendarHelperProvider { category: 'fa-cubes' }; - constructor(logger: CoreLoggerProvider, private courseProvider: CoreCourseProvider, + constructor(logger: CoreLoggerProvider, + private courseProvider: CoreCourseProvider, + private sitesProvider: CoreSitesProvider, private calendarProvider: AddonCalendarProvider) { this.logger = logger.getInstance('AddonCalendarHelperProvider'); } + /** + * Calculate some day data based on a list of events for that day. + * + * @param {any} day Day. + * @param {any[]} events Events. + */ + calculateDayData(day: any, events: any[]): void { + day.hasevents = events.length > 0; + day.haslastdayofevent = false; + + const types = {}; + events.forEach((event) => { + types[event.eventtype] = true; + + if (event.islastday) { + day.haslastdayofevent = true; + } + }); + + day.calendareventtypes = Object.keys(types); + } + /** * Check if current user can create/edit events. * @@ -155,4 +180,51 @@ export class AddonCalendarHelperProvider { return false; } + + /** + * Check if an event should be displayed based on the filter. + * + * @param {any} event Event object. + * @param {number} courseId Course ID to filter. + * @param {number} categoryId Category ID the course belongs to. + * @param {any} categories Categories indexed by ID. + * @return {boolean} Whether it should be displayed. + */ + shouldDisplayEvent(event: any, courseId: number, categoryId: number, categories: any): boolean { + if (event.eventtype == 'user' || event.eventtype == 'site') { + // User or site event, display it. + return true; + } + + if (event.eventtype == 'category') { + if (!event.categoryid || !Object.keys(categories).length) { + // We can't tell if the course belongs to the category, display them all. + return true; + } + + if (event.categoryid == categoryId) { + // The event is in the same category as the course, display it. + return true; + } + + // Check parent categories. + let category = categories[categoryId]; + while (category) { + if (!category.parent) { + // Category doesn't have parent, stop. + break; + } + + if (event.categoryid == category.parent) { + return true; + } + category = categories[category.parent]; + } + + return false; + } + + // Show the event if it is from site home or if it matches the selected course. + return event.courseid === this.sitesProvider.getSiteHomeId() || event.courseid == courseId; + } } diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 19cb0008db2..3e4a0a98405 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -106,9 +106,13 @@ "addon.calendar.eventname": "Event title", "addon.calendar.eventstarttime": "Start time", "addon.calendar.eventtype": "Event type", + "addon.calendar.fri": "Fri", + "addon.calendar.friday": "Friday", "addon.calendar.gotoactivity": "Go to activity", "addon.calendar.invalidtimedurationminutes": "The duration in minutes you have entered is invalid. Please enter the duration in minutes greater than 0 or select no duration.", "addon.calendar.invalidtimedurationuntil": "The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.", + "addon.calendar.mon": "Mon", + "addon.calendar.monday": "Monday", "addon.calendar.newevent": "New event", "addon.calendar.noevents": "There are no events", "addon.calendar.nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event", @@ -118,7 +122,15 @@ "addon.calendar.repeateditthis": "Apply changes to this event only", "addon.calendar.repeatevent": "Repeat this event", "addon.calendar.repeatweeksl": "Repeat weekly, creating altogether", + "addon.calendar.sat": "Sat", + "addon.calendar.saturday": "Saturday", "addon.calendar.setnewreminder": "Set a new reminder", + "addon.calendar.sun": "Sun", + "addon.calendar.sunday": "Sunday", + "addon.calendar.thu": "Thu", + "addon.calendar.thursday": "Thursday", + "addon.calendar.tue": "Tue", + "addon.calendar.tuesday": "Tuesday", "addon.calendar.typecategory": "Category event", "addon.calendar.typeclose": "Close event", "addon.calendar.typecourse": "Course event", @@ -128,6 +140,8 @@ "addon.calendar.typeopen": "Open event", "addon.calendar.typesite": "Site event", "addon.calendar.typeuser": "User event", + "addon.calendar.wed": "Wed", + "addon.calendar.wednesday": "Wednesday", "addon.competency.activities": "Activities", "addon.competency.competencies": "Competencies", "addon.competency.competenciesmostoftennotproficientincourse": "Competencies most often not proficient in this course", @@ -1658,6 +1672,7 @@ "core.notingroup": "Sorry, but you need to be part of a group to see this page.", "core.notsent": "Not sent", "core.now": "now", + "core.nummore": "{{$a}} more", "core.numwords": "{{$a}} words", "core.offline": "Offline", "core.ok": "OK", diff --git a/src/lang/en.json b/src/lang/en.json index 4ec6a60a48e..4f1d092cf08 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -184,6 +184,7 @@ "notingroup": "Sorry, but you need to be part of a group to see this page.", "notsent": "Not sent", "now": "now", + "nummore": "{{$a}} more", "numwords": "{{$a}} words", "offline": "Offline", "ok": "OK", diff --git a/src/theme/variables.scss b/src/theme/variables.scss index 919afa355da..0062ebe0961 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -28,6 +28,7 @@ $green: #5e8100; // Accent. $red: #cb3d4d; $orange: #f98012; // Accent (never text). $yellow: #fbad1a; // Accent (never text). +$purple: #8e24aa; // Accent (never text). $core-color: $orange; // Branded apps customization From b202d92dc35ea79c3930c3c1465d3202b1529405 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 3 Jul 2019 15:59:27 +0200 Subject: [PATCH 072/241] MOBILE-3021 calendar: Display offline events too --- .../calendar/addon-calendar-calendar.html | 16 +- .../components/calendar/calendar.scss | 9 + .../calendar/components/calendar/calendar.ts | 229 ++++++++++++++--- src/addon/calendar/pages/index/index.html | 8 +- src/addon/calendar/pages/index/index.ts | 240 +++++++++++++++--- .../calendar/providers/calendar-offline.ts | 12 + src/addon/calendar/providers/helper.ts | 49 +++- 7 files changed, 494 insertions(+), 69 deletions(-) diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index 04d35dfda85..a866c4be2fb 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -19,7 +19,7 @@ - + @@ -38,11 +38,15 @@
-

- - {{event.name}} -

-

{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}

+ +

+ + + + {{event.name}} +

+
+

{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}

diff --git a/src/addon/calendar/components/calendar/calendar.scss b/src/addon/calendar/components/calendar/calendar.scss index 853cff23a49..c3a20bf9e81 100644 --- a/src/addon/calendar/components/calendar/calendar.scss +++ b/src/addon/calendar/components/calendar/calendar.scss @@ -13,6 +13,15 @@ ion-app.app-root addon-calendar-calendar { .addon-calendar-day-events { @include text-align('start'); + + ion-icon { + @include margin-horizontal(null, 2px); + font-size: 1em; + } + } + + .addon-calendar-event { + cursor: pointer; } .calendar_event_type { diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 9bb41721ece..05386348389 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy, OnInit, Input, OnChanges, SimpleChange } from '@angular/core'; +import { Component, OnDestroy, OnInit, Input, OnChanges, SimpleChange, Output, EventEmitter } from '@angular/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; @@ -20,6 +20,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; /** @@ -35,6 +36,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest @Input() courseId: number | string; @Input() categoryId: number | string; // Category ID the course belongs to. @Input() canNavigate?: string | boolean; // Whether to include arrows to change the month. Defaults to true. + @Output() onEventClicked = new EventEmitter(); periodName: string; weekDays: any[]; @@ -45,16 +47,39 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest protected month: number; protected categoriesRetrieved = false; protected categories = {}; + protected currentSiteId: string; + protected offlineEvents: {[monthId: string]: {[day: number]: any[]}} = {}; // Offline events classified in month & day. + protected offlineEditedEventsIds = []; // IDs of events edited in offline. + protected deletedEvents = []; // Events deleted in offline. + + // Observers. + protected undeleteEventObserver: any; constructor(eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, private calendarProvider: AddonCalendarProvider, private calendarHelper: AddonCalendarHelperProvider, + private calendarOffline: AddonCalendarOfflineProvider, private domUtils: CoreDomUtilsProvider, private timeUtils: CoreTimeUtilsProvider, private utils: CoreUtilsProvider, private coursesProvider: CoreCoursesProvider) { + this.currentSiteId = sitesProvider.getCurrentSiteId(); + + // Listen for events "undeleted" (offline). + this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => { + if (data && data.eventId) { + // Mark it as undeleted, no need to refresh. + this.undeleteEvent(data.eventId); + + // Remove it from the list of deleted events if it's there. + const index = this.deletedEvents.indexOf(data.eventId); + if (index != -1) { + this.deletedEvents.splice(index, 1); + } + } + }, this.currentSiteId); } /** @@ -76,44 +101,78 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest ngOnChanges(changes: {[name: string]: SimpleChange}): void { if ((changes.courseId || changes.categoryId) && this.weeks) { - const courseId = this.courseId ? Number(this.courseId) : undefined, - categoryId = this.categoryId ? Number(this.categoryId) : undefined; - - this.filterEvents(courseId, categoryId); + this.filterEvents(); } } /** * Fetch contacts. * - * @param {boolean} [refresh=false] True if we are refreshing contacts, false if we are loading more. + * @param {boolean} [refresh=false] True if we are refreshing events. * @return {Promise} Promise resolved when done. */ fetchData(refresh: boolean = false): Promise { - const courseId = this.courseId ? Number(this.courseId) : undefined, - categoryId = this.categoryId ? Number(this.categoryId) : undefined, - promises = []; + const promises = []; promises.push(this.loadCategories()); - promises.push(this.calendarProvider.getMonthlyEvents(this.year, this.month, courseId, categoryId).then((result) => { + // Get offline events. + promises.push(this.calendarOffline.getAllEditedEvents().then((events) => { + // Format data. + events.forEach((event) => { + event.offline = true; + this.calendarHelper.formatEventData(event); + }); - // Calculate the period name. We don't use the one in result because it's in server's language. - this.periodName = this.timeUtils.userDate(new Date(this.year, this.month - 1).getTime(), 'core.strftimemonthyear'); + // Classify them by month. + this.offlineEvents = this.calendarHelper.classifyIntoMonths(events); - this.weekDays = this.calendarProvider.getWeekDays(result.daynames[0].dayno); - this.weeks = result.weeks; + // Get the IDs of events edited in offline. + const filtered = events.filter((event) => { + return event.id > 0; + }); + this.offlineEditedEventsIds = filtered.map((event) => { + return event.id; + }); + })); - this.filterEvents(courseId, categoryId); + // Get events deleted in offline. + promises.push(this.calendarOffline.getAllDeletedEventsIds().then((ids) => { + this.deletedEvents = ids; })); - return Promise.all(promises).catch((error) => { + return Promise.all(promises).then(() => { + return this.fetchEvents(); + }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); }).finally(() => { this.loaded = true; }); } + /** + * Fetch the events for current month. + * + * @return {Promise} Promise resolved when done. + */ + fetchEvents(): Promise { + // Don't pass courseId and categoryId, we'll filter them locally. + return this.calendarProvider.getMonthlyEvents(this.year, this.month).then((result) => { + + // Calculate the period name. We don't use the one in result because it's in server's language. + this.periodName = this.timeUtils.userDate(new Date(this.year, this.month - 1).getTime(), 'core.strftimemonthyear'); + + this.weekDays = this.calendarProvider.getWeekDays(result.daynames[0].dayno); + this.weeks = result.weeks; + + // Merge the online events with offline data. + this.mergeEvents(); + + // Filter events by course. + this.filterEvents(); + }); + } + /** * Load categories to be able to filter events. * @@ -140,11 +199,10 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest /** * Filter events to only display events belonging to a certain course. - * - * @param {number} courseId Course ID. - * @param {number} categoryId Category the course belongs to. */ - filterEvents(courseId: number, categoryId: number): void { + filterEvents(): void { + const courseId = this.courseId ? Number(this.courseId) : undefined, + categoryId = this.categoryId ? Number(this.categoryId) : undefined; this.weeks.forEach((week) => { week.days.forEach((day) => { @@ -165,9 +223,11 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest /** * Refresh events. * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. * @return {Promise} Promise resolved when done. */ - refreshData(): Promise { + refreshData(sync?: boolean, showErrors?: boolean): Promise { const promises = []; promises.push(this.calendarProvider.invalidateMonthlyEvents(this.year, this.month)); @@ -184,38 +244,145 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest * Load next month. */ loadNext(): void { - if (this.month === 12) { - this.month = 1; - this.year++; - } else { - this.month++; - } + this.increaseMonth(); this.loaded = false; - this.fetchData(); + this.fetchEvents().catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + this.decreaseMonth(); + }).finally(() => { + this.loaded = true; + }); } /** * Load previous month. */ loadPrevious(): void { + this.decreaseMonth(); + + this.loaded = false; + + this.fetchEvents().catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + this.increaseMonth(); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * An event was clicked. + * + * @param {any} event Event. + */ + eventClicked(event: any): void { + this.onEventClicked.emit(event.id); + } + + /** + * Decrease the current month. + */ + protected decreaseMonth(): void { if (this.month === 1) { this.month = 12; this.year--; } else { this.month--; } + } - this.loaded = false; + /** + * Increase the current month. + */ + protected increaseMonth(): void { + if (this.month === 12) { + this.month = 1; + this.year++; + } else { + this.month++; + } + } - this.fetchData(); + /** + * Merge online events with the offline events of that period. + */ + protected mergeEvents(): void { + const monthOfflineEvents = this.offlineEvents[this.calendarHelper.getMonthId(this.year, this.month)]; + + if (!monthOfflineEvents && !this.deletedEvents.length) { + // No offline events, nothing to merge. + return; + } + + this.weeks.forEach((week) => { + week.days.forEach((day) => { + + if (this.deletedEvents.length) { + // Mark as deleted the events that were deleted in offline. + day.events.forEach((event) => { + event.deleted = this.deletedEvents.indexOf(event.id) != -1; + }); + } + + if (this.offlineEditedEventsIds.length) { + // Remove the online events that were modified in offline. + day.events = day.events.filter((event) => { + return this.offlineEditedEventsIds.indexOf(event.id) == -1; + }); + } + + if (monthOfflineEvents && monthOfflineEvents[day.mday]) { + // Add the offline events (either new or edited). + day.events = this.sortEvents(day.events.concat(monthOfflineEvents[day.mday])); + } + }); + }); + } + + /** + * Sort events by timestart. + * + * @param {any[]} events List to sort. + */ + protected sortEvents(events: any[]): any[] { + return events.sort((a, b) => { + if (a.timestart == b.timestart) { + return a.timeduration - b.timeduration; + } + + return a.timestart - b.timestart; + }); + } + + /** + * Undelete a certain event. + * + * @param {number} eventId Event ID. + */ + protected undeleteEvent(eventId: number): void { + if (!this.weeks) { + return; + } + + this.weeks.forEach((week) => { + week.days.forEach((day) => { + const event = day.events.find((event) => { + return event.id == eventId; + }); + + if (event) { + event.deleted = false; + } + }); + }); } /** * Component destroyed. */ ngOnDestroy(): void { - // @todo + this.undeleteEventObserver && this.undeleteEventObserver.off(); } } diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html index 42c3cd4b927..fa0877b46da 100644 --- a/src/addon/calendar/pages/index/index.html +++ b/src/addon/calendar/pages/index/index.html @@ -7,6 +7,7 @@ + @@ -16,7 +17,12 @@ - + + + {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }} + + + diff --git a/src/addon/calendar/pages/index/index.ts b/src/addon/calendar/pages/index/index.ts index 9ab19cd7e3a..8f134ed904c 100644 --- a/src/addon/calendar/pages/index/index.ts +++ b/src/addon/calendar/pages/index/index.ts @@ -12,16 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, OnInit, OnDestroy, ViewChild, NgZone } from '@angular/core'; import { IonicPage, NavParams, NavController, PopoverController } from 'ionic-angular'; +import { CoreAppProvider } from '@providers/app'; +import { CoreEventsProvider } from '@providers/events'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarCalendarComponent } from '../../components/calendar/calendar'; +import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; import { TranslateService } from '@ngx-translate/core'; +import { Network } from '@ionic-native/network'; /** * Page that displays the calendar events. @@ -31,7 +37,7 @@ import { TranslateService } from '@ngx-translate/core'; selector: 'page-addon-calendar-index', templateUrl: 'index.html', }) -export class AddonCalendarIndexPage implements OnInit { +export class AddonCalendarIndexPage implements OnInit, OnDestroy { @ViewChild(AddonCalendarCalendarComponent) calendarComponent: AddonCalendarCalendarComponent; protected allCourses = { @@ -39,6 +45,18 @@ export class AddonCalendarIndexPage implements OnInit { fullname: this.translate.instant('core.fulllistofcourses'), category: -1 }; + protected eventId: number; + protected currentSiteId: string; + + // Observers. + protected newEventObserver: any; + protected discardedObserver: any; + protected editEventObserver: any; + protected deleteEventObserver: any; + protected undeleteEventObserver: any; + protected syncObserver: any; + protected manualSyncObserver: any; + protected onlineObserver: any; courseId: number; categoryId: number; @@ -46,63 +64,177 @@ export class AddonCalendarIndexPage implements OnInit { courses: any[]; notificationsEnabled = false; loaded = false; + hasOffline = false; + isOnline = false; + syncIcon: string; constructor(localNotificationsProvider: CoreLocalNotificationsProvider, navParams: NavParams, + network: Network, + zone: NgZone, + sitesProvider: CoreSitesProvider, private navCtrl: NavController, private domUtils: CoreDomUtilsProvider, private calendarProvider: AddonCalendarProvider, + private calendarOffline: AddonCalendarOfflineProvider, private calendarHelper: AddonCalendarHelperProvider, + private calendarSync: AddonCalendarSyncProvider, private translate: TranslateService, + private eventsProvider: CoreEventsProvider, private coursesProvider: CoreCoursesProvider, - private popoverCtrl: PopoverController) { + private popoverCtrl: PopoverController, + private appProvider: CoreAppProvider) { this.courseId = navParams.get('courseId'); + this.eventId = navParams.get('eventId') || false; this.notificationsEnabled = localNotificationsProvider.isAvailable(); + this.currentSiteId = sitesProvider.getCurrentSiteId(); + + // Listen for events added. When an event is added, reload the data. + this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { + if (data && data.event) { + this.loaded = false; + this.refreshData(true, false); + } + }, this.currentSiteId); + + // Listen for new event discarded event. When it does, reload the data. + this.discardedObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => { + this.loaded = false; + this.refreshData(true, false); + }, this.currentSiteId); + + // Listen for events edited. When an event is edited, reload the data. + this.editEventObserver = eventsProvider.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => { + if (data && data.event) { + this.loaded = false; + this.refreshData(true, false); + } + }, this.currentSiteId); + + // Refresh data if calendar events are synchronized automatically. + this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => { + this.loaded = false; + this.refreshData(); + }, this.currentSiteId); + + // Refresh data if calendar events are synchronized manually but not by this page. + this.manualSyncObserver = eventsProvider.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => { + if (data && data.source != 'index') { + this.loaded = false; + this.refreshData(); + } + }, this.currentSiteId); + + // Update the events when an event is deleted. + this.deleteEventObserver = eventsProvider.on(AddonCalendarProvider.DELETED_EVENT_EVENT, (data) => { + this.loaded = false; + this.refreshData(); + }, this.currentSiteId); + + // Update the "hasOffline" property if an event deleted in offline is restored. + this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => { + this.calendarOffline.hasOfflineData().then((hasOffline) => { + this.hasOffline = hasOffline; + }); + }, this.currentSiteId); + + // Refresh online status when changes. + this.onlineObserver = network.onchange().subscribe(() => { + // Execute the callback in the Angular zone, so change detection doesn't stop working. + zone.run(() => { + this.isOnline = this.appProvider.isOnline(); + }); + }); } /** * View loaded. */ ngOnInit(): void { - this.fetchData(); + if (this.eventId) { + // There is an event to load, open the event in a new state. + this.gotoEvent(this.eventId); + } + + this.fetchData(true, false); } /** * Fetch all the data required for the view. * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. * @return {Promise} Promise resolved when done. */ - fetchData(): Promise { - const promises = []; + fetchData(sync?: boolean, showErrors?: boolean): Promise { - // Load courses for the popover. - promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { - // Add "All courses". - courses.unshift(this.allCourses); - this.courses = courses; + this.syncIcon = 'spinner'; + this.isOnline = this.appProvider.isOnline(); - if (this.courseId) { - // Search the course to get the category. - const course = this.courses.find((course) => { - return course.id == this.courseId; - }); + let promise; - if (course) { - this.categoryId = course.category; + if (sync) { + // Try to synchronize offline events. + promise = this.calendarSync.syncEvents().then((result) => { + if (result.warnings && result.warnings.length) { + this.domUtils.showErrorModal(result.warnings[0]); } - } - })); - // Check if user can create events. - promises.push(this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { - this.canCreate = canEdit; - })); + if (result.updated) { + // Trigger a manual sync event. + result.source = 'index'; + + this.eventsProvider.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId); + } + }).catch((error) => { + if (showErrors) { + this.domUtils.showErrorModalDefault(error, 'core.errorsync', true); + } + }); + } else { + promise = Promise.resolve(); + } + + return promise.then(() => { + const promises = []; - return Promise.all(promises).catch((error) => { + this.hasOffline = false; + + // Load courses for the popover. + promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { + // Add "All courses". + courses.unshift(this.allCourses); + this.courses = courses; + + if (this.courseId) { + // Search the course to get the category. + const course = this.courses.find((course) => { + return course.id == this.courseId; + }); + + if (course) { + this.categoryId = course.category; + } + } + })); + + // Check if user can create events. + promises.push(this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { + this.canCreate = canEdit; + })); + + // Check if there is offline data. + promises.push(this.calendarOffline.hasOfflineData().then((hasOffline) => { + this.hasOffline = hasOffline; + })); + + return Promise.all(promises); + }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); }).finally(() => { this.loaded = true; + this.syncIcon = 'sync'; }); } @@ -110,13 +242,31 @@ export class AddonCalendarIndexPage implements OnInit { * Refresh the data. * * @param {any} [refresher] Refresher. + * @param {Function} [done] Function to call when done. + * @param {boolean} [showErrors] Whether to show sync errors to the user. * @return {Promise} Promise resolved when done. */ - doRefresh(refresher?: any): void { - if (!this.loaded) { - return; + doRefresh(refresher?: any, done?: () => void, showErrors?: boolean): Promise { + if (this.loaded) { + return this.refreshData(true, showErrors).finally(() => { + refresher && refresher.complete(); + done && done(); + }); } + return Promise.resolve(); + } + + /** + * Refresh the data. + * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + refreshData(sync?: boolean, showErrors?: boolean): Promise { + this.syncIcon = 'spinner'; + const promises = []; promises.push(this.calendarProvider.invalidateAllowedEventTypes().then(() => { @@ -126,11 +276,27 @@ export class AddonCalendarIndexPage implements OnInit { // Refresh the sub-component. promises.push(this.calendarComponent.refreshData()); - Promise.all(promises).finally(() => { - refresher && refresher.complete(); + return Promise.all(promises).finally(() => { + return this.fetchData(sync, showErrors); }); } + /** + * Navigate to a particular event. + * + * @param {number} eventId Event to load. + */ + gotoEvent(eventId: number): void { + if (eventId < 0) { + // It's an offline event, go to the edit page. + this.openEdit(eventId); + } else { + this.navCtrl.push('AddonCalendarEventPage', { + id: eventId + }); + } + } + /** * Show the context menu. * @@ -182,4 +348,18 @@ export class AddonCalendarIndexPage implements OnInit { openSettings(): void { this.navCtrl.push('AddonCalendarSettingsPage'); } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.newEventObserver && this.newEventObserver.off(); + this.discardedObserver && this.discardedObserver.off(); + this.editEventObserver && this.editEventObserver.off(); + this.deleteEventObserver && this.deleteEventObserver.off(); + this.undeleteEventObserver && this.undeleteEventObserver.off(); + this.syncObserver && this.syncObserver.off(); + this.manualSyncObserver && this.manualSyncObserver.off(); + this.onlineObserver && this.onlineObserver.unsubscribe(); + } } diff --git a/src/addon/calendar/providers/calendar-offline.ts b/src/addon/calendar/providers/calendar-offline.ts index 3d48baa38a7..ee06f75c98f 100644 --- a/src/addon/calendar/providers/calendar-offline.ts +++ b/src/addon/calendar/providers/calendar-offline.ts @@ -280,6 +280,18 @@ export class AddonCalendarOfflineProvider { }); } + /** + * Check whether there's offline data for a site. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if has offline data, false otherwise. + */ + hasOfflineData(siteId?: string): Promise { + return this.getAllEventsIds(siteId).then((ids) => { + return ids.length > 0; + }); + } + /** * Check if an event is deleted. * diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index 94cfa1e9386..225ec7ffb98 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -18,6 +18,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreCourseProvider } from '@core/course/providers/course'; import { AddonCalendarProvider } from './calendar'; import { CoreConstants } from '@core/constants'; +import * as moment from 'moment'; /** * Service that provides some features regarding lists of courses and categories. @@ -85,6 +86,41 @@ export class AddonCalendarHelperProvider { }); } + /** + * Classify events into their respective months and days. If an event duration covers more than one day, + * it will be included in all the days it lasts. + * + * @param {any[]} events Events to classify. + * @return {{[monthId: string]: {[day: number]: any[]}}} Object with the classified events. + */ + classifyIntoMonths(events: any[]): {[monthId: string]: {[day: number]: any[]}} { + + const result = {}; + + events.forEach((event) => { + const treatedDay = moment(new Date(event.timestart * 1000)), + endDay = moment(new Date((event.timestart + (event.timeduration || 0)) * 1000)); + + // Add the event to all the days it lasts. + while (!treatedDay.isAfter(endDay, 'day')) { + const monthId = this.getMonthId(treatedDay.year(), treatedDay.month() + 1), + day = treatedDay.date(); + + if (!result[monthId]) { + result[monthId] = {}; + } + if (!result[monthId][day]) { + result[monthId][day] = []; + } + result[monthId][day].push(event); + + treatedDay.add(1, 'day'); // Treat next day. + } + }); + + return result; + } + /** * Convenience function to format some event data to be rendered. * @@ -97,7 +133,7 @@ export class AddonCalendarHelperProvider { e.moduleIcon = e.icon; } - if (e.id < 0) { + if (typeof e.duration != 'undefined') { // It's an offline event, add some calculated data. e.format = 1; e.visible = 1; @@ -140,6 +176,17 @@ export class AddonCalendarHelperProvider { return options; } + /** + * Get the month "id" (year + month). + * + * @param {number} year Year. + * @param {number} month Month. + * @return {string} The "id". + */ + getMonthId(year: number, month: number): string { + return year + '#' + month; + } + /** * Check if the data of an event has changed. * From aa1de96f33011d8ad9ecb27ef9cd7e2ec3698abf Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Tue, 16 Jul 2019 09:49:39 +0200 Subject: [PATCH 073/241] MOBILE-3053 rte: Fix keyboard is closed and reopened in iOS --- .../core-rich-text-editor.html | 30 +++++++++--------- .../rich-text-editor/rich-text-editor.ts | 31 +++++++++++++------ 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/components/rich-text-editor/core-rich-text-editor.html b/src/components/rich-text-editor/core-rich-text-editor.html index 7874c0162ea..c4b96e0fef5 100644 --- a/src/components/rich-text-editor/core-rich-text-editor.html +++ b/src/components/rich-text-editor/core-rich-text-editor.html @@ -4,78 +4,78 @@
- - - - - - - - - - - - - - -
diff --git a/src/components/rich-text-editor/rich-text-editor.ts b/src/components/rich-text-editor/rich-text-editor.ts index 45e572d37fc..44be93408f0 100644 --- a/src/components/rich-text-editor/rich-text-editor.ts +++ b/src/components/rich-text-editor/rich-text-editor.ts @@ -533,10 +533,8 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy * @param {any} $event Event data * @param {string} command Command to execute. */ - protected buttonAction($event: any, command: string): void { - $event.preventDefault(); - $event.stopPropagation(); - this.editorElement.focus(); + buttonAction($event: any, command: string): void { + this.stopBubble($event); if (command) { if (command.includes('|')) { @@ -553,8 +551,9 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy /** * Hide the toolbar. */ - hideToolbar(): void { - this.editorElement.focus(); + hideToolbar($event: any): void { + this.stopBubble($event); + this.toolbarHidden = true; } @@ -566,26 +565,38 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy this.toolbarHidden = false; } + /** + * Stop event default and propagation. + * + * @param {Event} event Event. + */ + stopBubble(event: Event): void { + event.preventDefault(); + event.stopPropagation(); + } + /** * Method that shows the next toolbar buttons. */ - toolbarNext(): void { + toolbarNext($event: any): void { + this.stopBubble($event); + if (!this.toolbarNextHidden) { const currentIndex = this.toolbarSlides.getActiveIndex() || 0; this.toolbarSlides.slideTo(currentIndex + this.numToolbarButtons); } - this.editorElement.focus(); } /** * Method that shows the previous toolbar buttons. */ - toolbarPrev(): void { + toolbarPrev($event: any): void { + this.stopBubble($event); + if (!this.toolbarPrevHidden) { const currentIndex = this.toolbarSlides.getActiveIndex() || 0; this.toolbarSlides.slideTo(currentIndex - this.numToolbarButtons); } - this.editorElement.focus(); } /** From 66cc892794cefaf6829b784b171cf9961f8ab3c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 18 Jul 2019 16:49:20 +0200 Subject: [PATCH 074/241] MOBILE-3053 rte: Fix toolbar styles in iOS --- src/components/rich-text-editor/rich-text-editor.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/rich-text-editor/rich-text-editor.scss b/src/components/rich-text-editor/rich-text-editor.scss index 0cf3a59606d..9460297fd17 100644 --- a/src/components/rich-text-editor/rich-text-editor.scss +++ b/src/components/rich-text-editor/rich-text-editor.scss @@ -80,6 +80,8 @@ ion-app.app-root core-rich-text-editor { align-items: center; width: 36px; height: 36px; + padding-right: 6px; + padding-left: 6px; margin: 0 auto; font-size: 18px; background-color: $white; From bff2c111f3d565be7163cb52c7a247b062ae29e3 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Thu, 18 Jul 2019 16:51:29 +0200 Subject: [PATCH 075/241] MOBILE-3053 rte: Fix toolbar sometimes has only one button --- src/components/rich-text-editor/rich-text-editor.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/rich-text-editor/rich-text-editor.ts b/src/components/rich-text-editor/rich-text-editor.ts index 44be93408f0..b0261ba75dc 100644 --- a/src/components/rich-text-editor/rich-text-editor.ts +++ b/src/components/rich-text-editor/rich-text-editor.ts @@ -608,14 +608,15 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy return; } - if (!(this.toolbarSlides as any)._init) { - // Slides is not initialized yet, try later. + const width = this.domUtils.getElementWidth(this.toolbar.nativeElement); + + if (!(this.toolbarSlides as any)._init || !width) { + // Slides is not initialized or width is not available yet, try later. setTimeout(this.updateToolbarButtons.bind(this), 100); return; } - const width = this.domUtils.getElementWidth(this.toolbar.nativeElement); if (width > this.toolbarSlides.length() * this.toolbarButtonWidth) { this.numToolbarButtons = this.toolbarSlides.length(); this.toolbarArrows = false; From 63d18cc2f58498a67dca81f88cabf5006fba0747 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Thu, 18 Jul 2019 16:53:34 +0200 Subject: [PATCH 076/241] MOBILE-3053 rte: Hide toolbar when editor loses focus --- src/components/rich-text-editor/core-rich-text-editor.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/rich-text-editor/core-rich-text-editor.html b/src/components/rich-text-editor/core-rich-text-editor.html index c4b96e0fef5..ba2bd897f53 100644 --- a/src/components/rich-text-editor/core-rich-text-editor.html +++ b/src/components/rich-text-editor/core-rich-text-editor.html @@ -1,7 +1,7 @@ -
+
- +
+ diff --git a/src/core/comments/pages/viewer/viewer.ts b/src/core/comments/pages/viewer/viewer.ts index 6652b879b7f..e9533a7d2d0 100644 --- a/src/core/comments/pages/viewer/viewer.ts +++ b/src/core/comments/pages/viewer/viewer.ts @@ -40,6 +40,7 @@ export class CoreCommentsViewerPage { area: string; page: number; title: string; + addCommentsAvailable = false; constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider, private domUtils: CoreDomUtilsProvider, private translate: TranslateService, @@ -51,13 +52,17 @@ export class CoreCommentsViewerPage { this.itemId = navParams.get('itemId'); this.area = navParams.get('area') || ''; this.page = navParams.get('page') || 0; - this.title = navParams.get('title') || this.translate.instant('core.comments'); + this.title = navParams.get('title') || this.translate.instant('core.comments.comments'); } /** * View loaded. */ ionViewDidLoad(): void { + this.commentsProvider.isAddCommentsAvailable().then((enabled) => { + this.addCommentsAvailable = enabled; + }); + this.fetchComments().finally(() => { this.commentsLoaded = true; }); @@ -84,7 +89,7 @@ export class CoreCommentsViewerPage { }); }).catch((error) => { if (error && this.component == 'assignsubmission_comments') { - this.domUtils.showAlertTranslated('core.notice', 'core.commentsnotworking'); + this.domUtils.showAlertTranslated('core.notice', 'core.comments.commentsnotworking'); } else { this.domUtils.showErrorModalDefault(error, this.translate.instant('core.error') + ': get_comments'); } diff --git a/src/core/comments/providers/comments.ts b/src/core/comments/providers/comments.ts index 9808a0c8323..cd671d84d1e 100644 --- a/src/core/comments/providers/comments.ts +++ b/src/core/comments/providers/comments.ts @@ -50,6 +50,24 @@ export class CoreCommentsProvider { }); } + /** + * Returns whether WS to add/delete comments are available in site. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with true if available, resolved with false or rejected otherwise. + * @since 3.8 + */ + isAddCommentsAvailable(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + // First check if it's disabled. + if (this.areCommentsDisabledInSite(site)) { + return false; + } + + return site.wsAvailable('core_comment_add_comments'); + }); + } + /** * Get cache key for get comments data WS calls. * diff --git a/src/lang/en.json b/src/lang/en.json index c4111f1be45..3aceb76c752 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -25,9 +25,6 @@ "clicktohideshow": "Click to expand or collapse", "clicktoseefull": "Click to see full contents.", "close": "Close", - "comments": "Comments", - "commentscount": "Comments ({{$a}})", - "commentsnotworking": "Comments cannot be retrieved", "completion-alt-auto-fail": "Completed: {{$a}} (did not achieve pass grade)", "completion-alt-auto-n": "Not completed: {{$a}}", "completion-alt-auto-n-override": "Not completed: {{$a.modname}} (set by {{$a.overrideuser}})", @@ -168,7 +165,6 @@ "never": "Never", "next": "Next", "no": "No", - "nocomments": "No comments", "nograde": "No grade", "none": "None", "nopasswordchangeforced": "You cannot proceed without changing your password.", From dc65d4a00deb65547c89e3ec176dfa93e7a84234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 1 Jul 2019 15:56:24 +0200 Subject: [PATCH 078/241] MOBILE-2877 comments: Invalidate comments count --- src/addon/blog/components/entries/entries.ts | 4 ++++ src/addon/mod/data/pages/entry/entry.ts | 4 ++++ .../comments/components/comments/comments.ts | 4 +--- src/core/comments/pages/viewer/viewer.ts | 2 +- src/core/comments/providers/comments.ts | 22 ++++++++++--------- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/addon/blog/components/entries/entries.ts b/src/addon/blog/components/entries/entries.ts index b66db02a32b..d737c8e1bfe 100644 --- a/src/addon/blog/components/entries/entries.ts +++ b/src/addon/blog/components/entries/entries.ts @@ -169,6 +169,10 @@ export class AddonBlogEntriesComponent implements OnInit { * @param {any} refresher Refresher instance. */ refresh(refresher?: any): void { + this.entries.forEach((entry) => { + this.commentsProvider.invalidateCommentsData('user', entry.userid, this.component, entry.id, 'format_blog'); + }); + this.blogProvider.invalidateEntries(this.filter).finally(() => { this.fetchEntries(true).finally(() => { if (refresher) { diff --git a/src/addon/mod/data/pages/entry/entry.ts b/src/addon/mod/data/pages/entry/entry.ts index 51e95ce9ce2..2a938cbb18a 100644 --- a/src/addon/mod/data/pages/entry/entry.ts +++ b/src/addon/mod/data/pages/entry/entry.ts @@ -218,6 +218,10 @@ export class AddonModDataEntryPage implements OnDestroy { promises.push(this.dataProvider.invalidateDatabaseData(this.courseId)); if (this.data) { + if (this.data.comments && this.entry && this.entry.id > 0 && this.commentsEnabled) { + promises.push(this.commentsProvider.invalidateCommentsData('module', this.data.coursemodule, 'mod_data', + this.entry.id, 'database_entry')); + } promises.push(this.dataProvider.invalidateEntryData(this.data.id, this.entryId)); promises.push(this.groupsProvider.invalidateActivityGroupInfo(this.data.coursemodule)); promises.push(this.dataProvider.invalidateEntriesData(this.data.id)); diff --git a/src/core/comments/components/comments/comments.ts b/src/core/comments/components/comments/comments.ts index fed935d1d5e..a0146c85ae3 100644 --- a/src/core/comments/components/comments/comments.ts +++ b/src/core/comments/components/comments/comments.ts @@ -31,7 +31,6 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { @Input() component: string; @Input() itemId: number; @Input() area = ''; - @Input() page = 0; @Input() title?: string; @Input() displaySpinner = true; // Whether to display the loading spinner. @Output() onLoading: EventEmitter; // Eevent that indicates whether the component is loading data. @@ -72,7 +71,7 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { */ ngOnChanges(changes: { [name: string]: SimpleChange }): void { // If something change, update the fields. - if (changes) { + if (changes && this.commentsLoaded) { this.fetchData(); } } @@ -108,7 +107,6 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { component: this.component, itemId: this.itemId, area: this.area, - page: this.page, title: this.title, }); } diff --git a/src/core/comments/pages/viewer/viewer.ts b/src/core/comments/pages/viewer/viewer.ts index e9533a7d2d0..74f31663a9c 100644 --- a/src/core/comments/pages/viewer/viewer.ts +++ b/src/core/comments/pages/viewer/viewer.ts @@ -51,8 +51,8 @@ export class CoreCommentsViewerPage { this.component = navParams.get('component'); this.itemId = navParams.get('itemId'); this.area = navParams.get('area') || ''; - this.page = navParams.get('page') || 0; this.title = navParams.get('title') || this.translate.instant('core.comments.comments'); + this.page = 0; } /** diff --git a/src/core/comments/providers/comments.ts b/src/core/comments/providers/comments.ts index cd671d84d1e..0279c711e43 100644 --- a/src/core/comments/providers/comments.ts +++ b/src/core/comments/providers/comments.ts @@ -76,12 +76,11 @@ export class CoreCommentsProvider { * @param {string} component Component name. * @param {number} itemId Associated id. * @param {string} [area=''] String comment area. Default empty. - * @param {number} [page=0] Page number (0 based). Default 0. * @return {string} Cache key. */ - protected getCommentsCacheKey(contextLevel: string, instanceId: number, component: string, - itemId: number, area: string = '', page: number = 0): string { - return this.getCommentsPrefixCacheKey(contextLevel, instanceId) + ':' + component + ':' + itemId + ':' + area + ':' + page; + protected getCommentsCacheKey(contextLevel: string, instanceId: number, component: string, itemId: number, + area: string = ''): string { + return this.getCommentsPrefixCacheKey(contextLevel, instanceId) + ':' + component + ':' + itemId + ':' + area; } /** @@ -107,8 +106,8 @@ export class CoreCommentsProvider { * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the comments. */ - getComments(contextLevel: string, instanceId: number, component: string, itemId: number, - area: string = '', page: number = 0, siteId?: string): Promise { + getComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', page: number = 0, + siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params: any = { contextlevel: contextLevel, @@ -120,7 +119,7 @@ export class CoreCommentsProvider { }; const preSets = { - cacheKey: this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, area, page), + cacheKey: this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, area), updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; @@ -142,14 +141,17 @@ export class CoreCommentsProvider { * @param {string} component Component name. * @param {number} itemId Associated id. * @param {string} [area=''] String comment area. Default empty. - * @param {number} [page=0] Page number (0 based). Default 0. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the data is invalidated. */ invalidateCommentsData(contextLevel: string, instanceId: number, component: string, itemId: number, - area: string = '', page: number = 0, siteId?: string): Promise { + area: string = '', siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.invalidateWsCacheForKey(this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, area, page)); + // This is done with starting with to avoid conflicts with previous keys that were including page. + site.invalidateWsCacheForKeyStartingWith(this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, + area) + ':'); + + return site.invalidateWsCacheForKey(this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, area)); }); } From 797b0d7931827b71fc1fcf0f88b6c5f49b0e5a18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 2 Jul 2019 12:46:59 +0200 Subject: [PATCH 079/241] MOBILE-2877 comments: Add comments pagination --- src/core/comments/comments.module.ts | 11 +++- .../comments/components/comments/comments.ts | 21 ++++--- .../components/comments/core-comments.html | 4 +- src/core/comments/pages/viewer/viewer.html | 4 +- src/core/comments/pages/viewer/viewer.ts | 41 +++++++++++-- src/core/comments/providers/comments.ts | 60 ++++++++++++++++++- 6 files changed, 120 insertions(+), 21 deletions(-) diff --git a/src/core/comments/comments.module.ts b/src/core/comments/comments.module.ts index 980e458b8aa..3dc800d0581 100644 --- a/src/core/comments/comments.module.ts +++ b/src/core/comments/comments.module.ts @@ -14,6 +14,7 @@ import { NgModule } from '@angular/core'; import { CoreCommentsProvider } from './providers/comments'; +import { CoreEventsProvider } from '@providers/events'; @NgModule({ declarations: [ @@ -24,4 +25,12 @@ import { CoreCommentsProvider } from './providers/comments'; CoreCommentsProvider ] }) -export class CoreCommentsModule {} +export class CoreCommentsModule { + constructor(eventsProvider: CoreEventsProvider) { + // Reset comments page size. + eventsProvider.on(CoreEventsProvider.LOGIN, () => { + CoreCommentsProvider.pageSize = null; + CoreCommentsProvider.pageSizeOK = false; + }); + } +} \ No newline at end of file diff --git a/src/core/comments/components/comments/comments.ts b/src/core/comments/components/comments/comments.ts index a0146c85ae3..b380d8f3243 100644 --- a/src/core/comments/components/comments/comments.ts +++ b/src/core/comments/components/comments/comments.ts @@ -36,7 +36,8 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { @Output() onLoading: EventEmitter; // Eevent that indicates whether the component is loading data. commentsLoaded = false; - commentsCount: number; + commentsCount: string; + countError = false; disabled = false; protected updateSiteObserver; @@ -84,22 +85,20 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { this.commentsLoaded = false; this.onLoading.emit(true); - this.commentsProvider.getComments(this.contextLevel, this.instanceId, this.component, this.itemId, this.area, this.page) - .then((comments) => { - this.commentsCount = comments && comments.length ? comments.length : 0; - }).catch(() => { - this.commentsCount = -1; - }).finally(() => { - this.commentsLoaded = true; - this.onLoading.emit(false); - }); + this.commentsProvider.getCommentsCount(this.contextLevel, this.instanceId, this.component, this.itemId, this.area) + .then((commentsCount) => { + this.commentsCount = commentsCount; + this.countError = parseInt(this.commentsCount, 10) < 0; + this.commentsLoaded = true; + this.onLoading.emit(false); + }); } /** * Opens the comments page. */ openComments(): void { - if (!this.disabled && this.commentsCount >= 0) { + if (!this.disabled && !this.countError) { // Open a new state with the interpolated contents. this.navCtrl.push('CoreCommentsViewerPage', { contextLevel: this.contextLevel, diff --git a/src/core/comments/components/comments/core-comments.html b/src/core/comments/components/comments/core-comments.html index 8642ffbb058..2c2c8efebcc 100644 --- a/src/core/comments/components/comments/core-comments.html +++ b/src/core/comments/components/comments/core-comments.html @@ -1,8 +1,8 @@ -
+
{{ 'core.comments.commentscount' | translate : {'$a': commentsCount} }}
-
+
{{ 'core.comments.commentsnotworking' | translate }}
diff --git a/src/core/comments/pages/viewer/viewer.html b/src/core/comments/pages/viewer/viewer.html index 41e56725e6c..cb825c4dd1e 100644 --- a/src/core/comments/pages/viewer/viewer.html +++ b/src/core/comments/pages/viewer/viewer.html @@ -20,9 +20,11 @@

{{ comment.fullname }}

+ + - + diff --git a/src/core/comments/pages/viewer/viewer.ts b/src/core/comments/pages/viewer/viewer.ts index 74f31663a9c..d253fbcb83b 100644 --- a/src/core/comments/pages/viewer/viewer.ts +++ b/src/core/comments/pages/viewer/viewer.ts @@ -40,7 +40,11 @@ export class CoreCommentsViewerPage { area: string; page: number; title: string; - addCommentsAvailable = false; + canLoadMore = false; + loadMoreError = false; + canAddComments = false; + + protected addCommentsAvailable = false; constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider, private domUtils: CoreDomUtilsProvider, private translate: TranslateService, @@ -74,11 +78,16 @@ export class CoreCommentsViewerPage { * @return {Promise} Resolved when done. */ protected fetchComments(): Promise { + this.loadMoreError = false; + // Get comments data. return this.commentsProvider.getComments(this.contextLevel, this.instanceId, this.component, this.itemId, - this.area, this.page).then((comments) => { - this.comments = comments; - this.comments.sort((a, b) => b.timecreated - a.timecreated); + this.area, this.page).then((response) => { + this.canAddComments = this.addCommentsAvailable && response.canpost; + + const comments = response.comments.sort((a, b) => b.timecreated - a.timecreated); + this.canLoadMore = comments.length >= CoreCommentsProvider.pageSize; + this.comments.forEach((comment) => { // Get the user profile image. this.userProvider.getProfile(comment.userid, undefined, true).then((user) => { @@ -87,7 +96,11 @@ export class CoreCommentsViewerPage { // Ignore errors. }); }); + + this.comments = this.comments.concat(comments); + }).catch((error) => { + this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. if (error && this.component == 'assignsubmission_comments') { this.domUtils.showAlertTranslated('core.notice', 'core.comments.commentsnotworking'); } else { @@ -96,6 +109,21 @@ export class CoreCommentsViewerPage { }); } + /** + * Function to load more cp,,emts. + * + * @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading. + * @return {Promise} Resolved when done. + */ + loadMore(infiniteComplete?: any): Promise { + this.page++; + this.canLoadMore = false; + + return this.fetchComments().finally(() => { + infiniteComplete && infiniteComplete(); + }); + } + /** * Refresh the comments. * @@ -103,7 +131,10 @@ export class CoreCommentsViewerPage { */ refreshComments(refresher: any): void { this.commentsProvider.invalidateCommentsData(this.contextLevel, this.instanceId, this.component, - this.itemId, this.area, this.page).finally(() => { + this.itemId, this.area).finally(() => { + this.page = 0; + this.comments = []; + return this.fetchComments().finally(() => { refresher.complete(); }); diff --git a/src/core/comments/providers/comments.ts b/src/core/comments/providers/comments.ts index 0279c711e43..e5634bf268d 100644 --- a/src/core/comments/providers/comments.ts +++ b/src/core/comments/providers/comments.ts @@ -23,6 +23,8 @@ import { CoreSite } from '@classes/site'; export class CoreCommentsProvider { protected ROOT_CACHE_KEY = 'mmComments:'; + static pageSize = null; + static pageSizeOK = false; // If true, the pageSize is definitive. If not, it's a temporal value to reduce WS calls. constructor(private sitesProvider: CoreSitesProvider) {} @@ -125,7 +127,7 @@ export class CoreCommentsProvider { return site.read('core_comment_get_comments', params, preSets).then((response) => { if (response.comments) { - return response.comments; + return response; } return Promise.reject(null); @@ -133,6 +135,62 @@ export class CoreCommentsProvider { }); } + /** + * Get comments count number to show ont he comments component. + * + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Comments count with plus sign if needed. + */ + getCommentsCount(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + + siteId = siteId ? siteId : this.sitesProvider.getCurrentSiteId(); + + // Convenience function to get comments number on a page. + const getCommentsPageCount = (page: number): Promise => { + return this.getComments(contextLevel, instanceId, component, itemId, area, page, siteId).then((response) => { + if (response.comments) { + // Update pageSize with the greatest count at the moment. + if (response.comments && response.comments.length > CoreCommentsProvider.pageSize) { + CoreCommentsProvider.pageSize = response.comments.length; + } + + return response.comments && response.comments.length ? response.comments.length : 0; + } + + return -1; + }).catch(() => { + return -1; + }); + }; + + return getCommentsPageCount(0).then((count) => { + if (CoreCommentsProvider.pageSizeOK && count >= CoreCommentsProvider.pageSize) { + // Page Size is ok, show + in case it reached the limit. + return (CoreCommentsProvider.pageSize - 1) + '+'; + } else if (count < 0 || (CoreCommentsProvider.pageSize && count < CoreCommentsProvider.pageSize)) { + return count + ''; + } + + // Call to update page size. + return getCommentsPageCount(1).then((countMore) => { + // Page limit was reached on the previous call. + if (countMore > 0) { + CoreCommentsProvider.pageSizeOK = true; + + return (CoreCommentsProvider.pageSize - 1) + '+'; + } + + return count + ''; + }); + }); + } + /** * Invalidates comments data. * From 79bdd4ed02f7ca76ff22b56cfb0f8ee053c61e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 2 Jul 2019 14:04:13 +0200 Subject: [PATCH 080/241] MOBILE-2877 comments: Add comments --- src/assets/lang/en.json | 2 + src/core/comments/comments.module.ts | 17 +- .../comments/components/comments/comments.ts | 2 +- src/core/comments/lang/en.json | 4 +- src/core/comments/pages/add/add.html | 22 ++ src/core/comments/pages/add/add.module.ts | 31 +++ src/core/comments/pages/add/add.ts | 82 +++++++ src/core/comments/pages/viewer/viewer.html | 30 ++- .../comments/pages/viewer/viewer.module.ts | 2 + src/core/comments/pages/viewer/viewer.ts | 202 +++++++++++++--- src/core/comments/providers/comments.ts | 105 ++++++++- src/core/comments/providers/offline.ts | 187 +++++++++++++++ .../comments/providers/sync-cron-handler.ts | 48 ++++ src/core/comments/providers/sync.ts | 221 ++++++++++++++++++ 14 files changed, 912 insertions(+), 43 deletions(-) create mode 100644 src/core/comments/pages/add/add.html create mode 100644 src/core/comments/pages/add/add.module.ts create mode 100644 src/core/comments/pages/add/add.ts create mode 100644 src/core/comments/providers/offline.ts create mode 100644 src/core/comments/providers/sync-cron-handler.ts create mode 100644 src/core/comments/providers/sync.ts diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 42b0102c135..4ada12bc37e 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1269,7 +1269,9 @@ "core.comments.comments": "Comments", "core.comments.commentscount": "Comments ({{$a}})", "core.comments.commentsnotworking": "Comments cannot be retrieved", + "core.comments.eventcommentcreated": "Comment created", "core.comments.nocomments": "No comments", + "core.comments.savecomment": "Save comment", "core.completion-alt-auto-fail": "Completed: {{$a}} (did not achieve pass grade)", "core.completion-alt-auto-n": "Not completed: {{$a}}", "core.completion-alt-auto-n-override": "Not completed: {{$a.modname}} (set by {{$a.overrideuser}})", diff --git a/src/core/comments/comments.module.ts b/src/core/comments/comments.module.ts index 3dc800d0581..d19d2ccf34e 100644 --- a/src/core/comments/comments.module.ts +++ b/src/core/comments/comments.module.ts @@ -13,8 +13,12 @@ // limitations under the License. import { NgModule } from '@angular/core'; -import { CoreCommentsProvider } from './providers/comments'; import { CoreEventsProvider } from '@providers/events'; +import { CoreCronDelegate } from '@providers/cron'; +import { CoreCommentsProvider } from './providers/comments'; +import { CoreCommentsOfflineProvider } from './providers/offline'; +import { CoreCommentsSyncCronHandler } from './providers/sync-cron-handler'; +import { CoreCommentsSyncProvider } from './providers/sync'; @NgModule({ declarations: [ @@ -22,15 +26,20 @@ import { CoreEventsProvider } from '@providers/events'; imports: [ ], providers: [ - CoreCommentsProvider + CoreCommentsProvider, + CoreCommentsOfflineProvider, + CoreCommentsSyncProvider, + CoreCommentsSyncCronHandler ] }) export class CoreCommentsModule { - constructor(eventsProvider: CoreEventsProvider) { + constructor(eventsProvider: CoreEventsProvider, cronDelegate: CoreCronDelegate, syncHandler: CoreCommentsSyncCronHandler) { // Reset comments page size. eventsProvider.on(CoreEventsProvider.LOGIN, () => { CoreCommentsProvider.pageSize = null; CoreCommentsProvider.pageSizeOK = false; }); + + cronDelegate.register(syncHandler); } -} \ No newline at end of file +} diff --git a/src/core/comments/components/comments/comments.ts b/src/core/comments/components/comments/comments.ts index b380d8f3243..6f7a2244479 100644 --- a/src/core/comments/components/comments/comments.ts +++ b/src/core/comments/components/comments/comments.ts @@ -103,7 +103,7 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { this.navCtrl.push('CoreCommentsViewerPage', { contextLevel: this.contextLevel, instanceId: this.instanceId, - component: this.component, + componentName: this.component, itemId: this.itemId, area: this.area, title: this.title, diff --git a/src/core/comments/lang/en.json b/src/core/comments/lang/en.json index 0eb28087747..b6c99e4c3cf 100644 --- a/src/core/comments/lang/en.json +++ b/src/core/comments/lang/en.json @@ -3,5 +3,7 @@ "comments": "Comments", "commentscount": "Comments ({{$a}})", "commentsnotworking": "Comments cannot be retrieved", - "nocomments": "No comments" + "eventcommentcreated": "Comment created", + "nocomments": "No comments", + "savecomment": "Save comment" } \ No newline at end of file diff --git a/src/core/comments/pages/add/add.html b/src/core/comments/pages/add/add.html new file mode 100644 index 00000000000..b5ca49440af --- /dev/null +++ b/src/core/comments/pages/add/add.html @@ -0,0 +1,22 @@ + + + {{ 'core.comments.addcomment' | translate }} + + + + + + + + + + +
+ +
+ +
diff --git a/src/core/comments/pages/add/add.module.ts b/src/core/comments/pages/add/add.module.ts new file mode 100644 index 00000000000..a6b6661a019 --- /dev/null +++ b/src/core/comments/pages/add/add.module.ts @@ -0,0 +1,31 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreCommentsAddPage } from './add'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + CoreCommentsAddPage + ], + imports: [ + CoreDirectivesModule, + IonicPageModule.forChild(CoreCommentsAddPage), + TranslateModule.forChild() + ] +}) +export class CoreCommentsAddPageModule {} diff --git a/src/core/comments/pages/add/add.ts b/src/core/comments/pages/add/add.ts new file mode 100644 index 00000000000..1d58db46f9f --- /dev/null +++ b/src/core/comments/pages/add/add.ts @@ -0,0 +1,82 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 } from 'ionic-angular'; +import { CoreAppProvider } from '@providers/app'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreCommentsProvider } from '../../providers/comments'; + +/** + * Component that displays a text area for composing a comment. + */ +@IonicPage({ segment: 'core-comments-add' }) +@Component({ + selector: 'page-core-comments-add', + templateUrl: 'add.html', +}) +export class CoreCommentsAddPage { + protected contextLevel: string; + protected instanceId: number; + protected componentName: string; + protected itemId: number; + protected area = ''; + + content = ''; + processing = false; + + constructor(params: NavParams, private viewCtrl: ViewController, private appProvider: CoreAppProvider, + private domUtils: CoreDomUtilsProvider, private commentsProvider: CoreCommentsProvider) { + this.contextLevel = params.get('contextLevel'); + this.instanceId = params.get('instanceId'); + this.componentName = params.get('componentName'); + this.itemId = params.get('itemId'); + this.area = params.get('area') || ''; + this.content = params.get('content') || ''; + } + + /** + * Send the comment or store it offline. + * + * @param {Event} e Event. + */ + addComment(e: Event): void { + e.preventDefault(); + e.stopPropagation(); + + this.appProvider.closeKeyboard(); + const loadingModal = this.domUtils.showModalLoading('core.sending', true); + // Freeze the add note button. + this.processing = true; + this.commentsProvider.addComment(this.content, this.contextLevel, this.instanceId, this.componentName, this.itemId, + this.area).then((commentsResponse) => { + this.viewCtrl.dismiss({comments: commentsResponse}).finally(() => { + this.domUtils.showToast(commentsResponse ? 'core.comments.eventcommentcreated' : 'core.datastoredoffline', true, + 3000); + }); + }).catch((error) => { + this.domUtils.showErrorModal(error); + this.processing = false; + }).finally(() => { + loadingModal.dismiss(); + }); + } + + /** + * Close modal. + */ + closeModal(): void { + this.viewCtrl.dismiss(); + } +} diff --git a/src/core/comments/pages/viewer/viewer.html b/src/core/comments/pages/viewer/viewer.html index cb825c4dd1e..812838695e2 100644 --- a/src/core/comments/pages/viewer/viewer.html +++ b/src/core/comments/pages/viewer/viewer.html @@ -1,20 +1,44 @@ + + + + + + - + +
+ + {{ 'core.thereisdatatosync' | translate:{$a: 'core.comments.comments' | translate | lowercase } }} +
+ + + + +

{{ offlineComment.fullname }}

+

+ {{ 'core.notsent' | translate }} +

+
+ + + +
+

{{ comment.fullname }}

-

{{ comment.time }}

+

{{ comment.timecreated * 1000 | coreFormatDate: 'strftimerecentfull' }}

@@ -25,7 +49,7 @@

{{ comment.fullname }}

- diff --git a/src/core/comments/pages/viewer/viewer.module.ts b/src/core/comments/pages/viewer/viewer.module.ts index ca526797011..3326cfe19aa 100644 --- a/src/core/comments/pages/viewer/viewer.module.ts +++ b/src/core/comments/pages/viewer/viewer.module.ts @@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreCommentsViewerPage } from './viewer'; import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; import { CoreCommentsComponentsModule } from '../../components/components.module'; @NgModule({ @@ -27,6 +28,7 @@ import { CoreCommentsComponentsModule } from '../../components/components.module imports: [ CoreComponentsModule, CoreDirectivesModule, + CorePipesModule, CoreCommentsComponentsModule, IonicPageModule.forChild(CoreCommentsViewerPage), TranslateModule.forChild() diff --git a/src/core/comments/pages/viewer/viewer.ts b/src/core/comments/pages/viewer/viewer.ts index d253fbcb83b..50bbf004f7d 100644 --- a/src/core/comments/pages/viewer/viewer.ts +++ b/src/core/comments/pages/viewer/viewer.ts @@ -12,13 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, ViewChild } from '@angular/core'; -import { IonicPage, Content, NavParams } from 'ionic-angular'; +import { Component, ViewChild, OnDestroy } from '@angular/core'; +import { IonicPage, Content, NavParams, ModalController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreEventsProvider } from '@providers/events'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreCommentsProvider } from '../../providers/comments'; +import { CoreCommentsOfflineProvider } from '../../providers/offline'; +import { CoreCommentsSyncProvider } from '../../providers/sync'; /** * Page that displays comments. @@ -28,14 +32,14 @@ import { CoreCommentsProvider } from '../../providers/comments'; selector: 'page-core-comments-viewer', templateUrl: 'viewer.html', }) -export class CoreCommentsViewerPage { +export class CoreCommentsViewerPage implements OnDestroy { @ViewChild(Content) content: Content; comments = []; commentsLoaded = false; contextLevel: string; instanceId: number; - component: string; + componentName: string; itemId: number; area: string; page: number; @@ -43,20 +47,48 @@ export class CoreCommentsViewerPage { canLoadMore = false; loadMoreError = false; canAddComments = false; + hasOffline = false; + refreshIcon = 'spinner'; + syncIcon = 'spinner'; + offlineComment: any; protected addCommentsAvailable = false; + protected syncObserver: any; + protected currentUser: any; - constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider, - private domUtils: CoreDomUtilsProvider, private translate: TranslateService, - private commentsProvider: CoreCommentsProvider) { + constructor(navParams: NavParams, private sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider, + private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private modalCtrl: ModalController, + private commentsProvider: CoreCommentsProvider, private offlineComments: CoreCommentsOfflineProvider, + eventsProvider: CoreEventsProvider, private commentsSync: CoreCommentsSyncProvider, + private textUtils: CoreTextUtilsProvider) { this.contextLevel = navParams.get('contextLevel'); this.instanceId = navParams.get('instanceId'); - this.component = navParams.get('component'); + this.componentName = navParams.get('componentName'); this.itemId = navParams.get('itemId'); this.area = navParams.get('area') || ''; this.title = navParams.get('title') || this.translate.instant('core.comments.comments'); this.page = 0; + + // Refresh data if comments are synchronized automatically. + this.syncObserver = eventsProvider.on(CoreCommentsSyncProvider.AUTO_SYNCED, (data) => { + if (data.contextLevel == this.contextLevel && data.instanceId == this.instanceId && + data.componentName == this.componentName && data.itemId == this.itemId && data.area == this.area) { + // Show the sync warnings. + this.showSyncWarnings(data.warnings); + + // Refresh the data. + this.commentsLoaded = false; + this.refreshIcon = 'spinner'; + this.syncIcon = 'spinner'; + + this.domUtils.scrollToTop(this.content); + + this.page = 0; + this.comments = []; + this.fetchComments(false); + } + }, sitesProvider.getCurrentSiteId()); } /** @@ -67,46 +99,78 @@ export class CoreCommentsViewerPage { this.addCommentsAvailable = enabled; }); - this.fetchComments().finally(() => { - this.commentsLoaded = true; - }); + this.fetchComments(true); } /** * Fetches the comments. * + * @param {boolean} sync When to resync notes. + * @param {boolean} [showErrors] When to display errors or not. * @return {Promise} Resolved when done. */ - protected fetchComments(): Promise { + protected fetchComments(sync: boolean, showErrors?: boolean): Promise { this.loadMoreError = false; - // Get comments data. - return this.commentsProvider.getComments(this.contextLevel, this.instanceId, this.component, this.itemId, - this.area, this.page).then((response) => { - this.canAddComments = this.addCommentsAvailable && response.canpost; + const promise = sync ? this.syncComment(showErrors) : Promise.resolve(); - const comments = response.comments.sort((a, b) => b.timecreated - a.timecreated); - this.canLoadMore = comments.length >= CoreCommentsProvider.pageSize; + return promise.catch(() => { + // Ignore errors. + }).then(() => { + return this.offlineComments.getComment(this.contextLevel, this.instanceId, this.componentName, this.itemId, + this.area).then((offlineComment) => { + this.hasOffline = !!offlineComment; + this.offlineComment = offlineComment; - this.comments.forEach((comment) => { - // Get the user profile image. - this.userProvider.getProfile(comment.userid, undefined, true).then((user) => { - comment.profileimageurl = user.profileimageurl; - }).catch(() => { - // Ignore errors. - }); + if (this.hasOffline && !this.currentUser) { + return this.userProvider.getProfile(this.sitesProvider.getCurrentSiteUserId(), undefined, true).then((user) => { + this.currentUser = user; + this.offlineComment.profileimageurl = user.profileimageurl; + this.offlineComment.fullname = user.fullname; + this.offlineComment.userid = user.id; + }).catch(() => { + // Ignore errors. + }); + } else if (this.hasOffline) { + this.offlineComment.profileimageurl = this.currentUser.profileimageurl; + this.offlineComment.fullname = this.currentUser.fullname; + this.offlineComment.userid = this.currentUser.id; + } }); + }).then(() => { + + // Get comments data. + return this.commentsProvider.getComments(this.contextLevel, this.instanceId, this.componentName, this.itemId, + this.area, this.page).then((response) => { + this.canAddComments = this.addCommentsAvailable && response.canpost; - this.comments = this.comments.concat(comments); + const comments = response.comments.sort((a, b) => b.timecreated - a.timecreated); + this.canLoadMore = comments.length >= CoreCommentsProvider.pageSize; + + this.comments.forEach((comment) => { + // Get the user profile image. + this.userProvider.getProfile(comment.userid, undefined, true).then((user) => { + comment.profileimageurl = user.profileimageurl; + }).catch(() => { + // Ignore errors. + }); + }); + this.comments = this.comments.concat(comments); + }); }).catch((error) => { this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. - if (error && this.component == 'assignsubmission_comments') { + if (error && this.componentName == 'assignsubmission_comments') { this.domUtils.showAlertTranslated('core.notice', 'core.comments.commentsnotworking'); } else { this.domUtils.showErrorModalDefault(error, this.translate.instant('core.error') + ': get_comments'); } + }).finally(() => { + this.commentsLoaded = true; + this.refreshIcon = 'refresh'; + this.syncIcon = 'sync'; }); + } /** @@ -119,7 +183,7 @@ export class CoreCommentsViewerPage { this.page++; this.canLoadMore = false; - return this.fetchComments().finally(() => { + return this.fetchComments(true).finally(() => { infiniteComplete && infiniteComplete(); }); } @@ -127,17 +191,89 @@ export class CoreCommentsViewerPage { /** * Refresh the comments. * - * @param {any} refresher Refresher. + * @param {boolean} showErrors Whether to display errors or not. + * @param {any} [refresher] Refresher. + * @return {Promise} Resolved when done. */ - refreshComments(refresher: any): void { - this.commentsProvider.invalidateCommentsData(this.contextLevel, this.instanceId, this.component, + refreshComments(showErrors: boolean, refresher?: any): Promise { + this.refreshIcon = 'spinner'; + this.syncIcon = 'spinner'; + + return this.commentsProvider.invalidateCommentsData(this.contextLevel, this.instanceId, this.componentName, this.itemId, this.area).finally(() => { this.page = 0; this.comments = []; - return this.fetchComments().finally(() => { - refresher.complete(); + return this.fetchComments(true, showErrors).finally(() => { + refresher && refresher.complete(); }); }); } + + /** + * Show sync warnings if any. + * + * @param {string[]} warnings the warnings + */ + private showSyncWarnings(warnings: string[]): void { + const message = this.textUtils.buildMessage(warnings); + if (message) { + this.domUtils.showErrorModal(message); + } + } + + /** + * Tries to synchronize comments. + * + * @param {boolean} showErrors Whether to display errors or not. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + private syncComment(showErrors: boolean): Promise { + return this.commentsSync.syncComment(this.contextLevel, this.instanceId, this.componentName, this.itemId, + this.area).then((warnings) => { + this.showSyncWarnings(warnings); + }).catch((error) => { + if (showErrors) { + this.domUtils.showErrorModalDefault(error, 'core.errorsync', true); + } + + return Promise.reject(null); + }); + } + + /** + * Add a new comment to the list. + * + * @param {Event} e Event. + */ + addComment(e: Event): void { + e.preventDefault(); + e.stopPropagation(); + + const params = { + contextLevel: this.contextLevel, + instanceId: this.instanceId, + componentName: this.componentName, + itemId: this.itemId, + area: this.area, + content: this.hasOffline ? this.offlineComment.content : '' + }; + + const modal = this.modalCtrl.create('CoreCommentsAddPage', params); + modal.onDidDismiss((data) => { + if (data && data.comments) { + this.comments = data.comments.concat(this.comments); + } else if (data && !data.comments) { + this.fetchComments(false); + } + }); + modal.present(); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.syncObserver && this.syncObserver.off(); + } } diff --git a/src/core/comments/providers/comments.ts b/src/core/comments/providers/comments.ts index e5634bf268d..a17748b08c3 100644 --- a/src/core/comments/providers/comments.ts +++ b/src/core/comments/providers/comments.ts @@ -13,8 +13,11 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { CoreAppProvider } from '@providers/app'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSitesProvider } from '@providers/sites'; import { CoreSite } from '@classes/site'; +import { CoreCommentsOfflineProvider } from './offline'; /** * Service that provides some features regarding comments. @@ -26,7 +29,107 @@ export class CoreCommentsProvider { static pageSize = null; static pageSizeOK = false; // If true, the pageSize is definitive. If not, it's a temporal value to reduce WS calls. - constructor(private sitesProvider: CoreSitesProvider) {} + constructor(private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, private appProvider: CoreAppProvider, + private commentsOffline: CoreCommentsOfflineProvider) {} + + /** + * Add a comment. + * + * @param {string} content Comment text. + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if comment was sent to server, false if stored in device. + */ + addComment(content: string, contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a note to be synchronized later. + const storeOffline = (): Promise => { + return this.commentsOffline.saveComment(content, contextLevel, instanceId, component, itemId, area, siteId).then(() => { + return Promise.resolve(false); + }); + }; + + if (!this.appProvider.isOnline()) { + // App is offline, store the note. + return storeOffline(); + } + + // Send note to server. + return this.addCommentOnline(content, contextLevel, instanceId, component, itemId, area, siteId).then((comments) => { + return comments; + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, the user cannot send the message so don't store it. + return Promise.reject(error); + } + + // Error sending note, store it to retry later. + return storeOffline(); + }); + } + + /** + * Add a comment. It will fail if offline or cannot connect. + * + * @param {string} content Comment text. + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when added, rejected otherwise. + */ + addCommentOnline(content: string, contextLevel: string, instanceId: number, component: string, itemId: number, + area: string = '', siteId?: string): Promise { + const comments = [ + { + contextlevel: contextLevel, + instanceid: instanceId, + component: component, + itemid: itemId, + area: area, + content: content + } + ]; + + return this.addCommentsOnline(comments, siteId).then((commentsResponse) => { + // A cooment was added, invalidate them. + return this.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId).catch(() => { + // Ignore errors. + }).then(() => { + return commentsResponse; + }); + }); + } + + /** + * Add several comments. It will fail if offline or cannot connect. + * + * @param {any[]} comments Comments to save. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when added, rejected otherwise. Promise resolved doesn't mean that comments + * have been added, the resolve param can contain errors for notes not sent. + */ + addCommentsOnline(comments: any[], siteId?: string): Promise { + if (!comments || !comments.length) { + return Promise.resolve(); + } + + return this.sitesProvider.getSite(siteId).then((site) => { + const data = { + comments: comments + }; + + return site.write('core_comment_add_comments', data); + }); + } /** * Check if Calendar is disabled in a certain site. diff --git a/src/core/comments/providers/offline.ts b/src/core/comments/providers/offline.ts new file mode 100644 index 00000000000..82dbd689472 --- /dev/null +++ b/src/core/comments/providers/offline.ts @@ -0,0 +1,187 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; + +/** + * Service to handle offline comments. + */ +@Injectable() +export class CoreCommentsOfflineProvider { + + // Variables for database. + static COMMENTS_TABLE = 'core_comments_offline_comments'; + protected siteSchema: CoreSiteSchema = { + name: 'CoreCommentsOfflineProvider', + version: 1, + tables: [ + { + name: CoreCommentsOfflineProvider.COMMENTS_TABLE, + columns: [ + { + name: 'contextlevel', + type: 'TEXT' + }, + { + name: 'instanceid', + type: 'INTEGER' + }, + { + name: 'component', + type: 'TEXT', + }, + { + name: 'itemid', + type: 'INTEGER' + }, + { + name: 'area', + type: 'TEXT' + }, + { + name: 'content', + type: 'TEXT' + }, + { + name: 'action', + type: 'TEXT' + }, + { + name: 'lastmodified', + type: 'INTEGER' + } + ], + primaryKeys: ['contextlevel', 'instanceid', 'component', 'itemid', 'area'] + } + ] + }; + + constructor( private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider) { + this.sitesProvider.registerSiteSchema(this.siteSchema); + } + + /** + * Delete a comment. + * + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + removeComment(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().deleteRecords(CoreCommentsOfflineProvider.COMMENTS_TABLE, { + contextlevel: contextLevel, + instanceid: instanceId, + component: component, + itemid: itemId, + area: area + }); + }); + } + + /** + * Get all offline comments. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with comments. + */ + getAllComments(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_TABLE); + }); + } + + /** + * Get an offline comment. + * + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the comments. + */ + getComment(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecord(CoreCommentsOfflineProvider.COMMENTS_TABLE, { + contextlevel: contextLevel, + instanceid: instanceId, + component: component, + itemid: itemId, + area: area + }); + }).catch(() => { + return false; + }); + } + + /** + * Check if there are offline comments. + * + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @return {Promise} Promise resolved with boolean: true if has offline comments, false otherwise. + */ + hasComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + return this.getComment(contextLevel, instanceId, component, itemId, area, siteId).then((comments) => { + return !!comments.length; + }); + } + + /** + * Save a comment to be sent later. + * + * @param {string} content Comment text. + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + saveComment(content: string, contextLevel: string, instanceId: number, component: string, itemId: number, + area: string = '', siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const now = this.timeUtils.timestamp(); + const data = { + contextlevel: contextLevel, + instanceid: instanceId, + component: component, + itemid: itemId, + area: area, + content: content, + action: 'add', + lastmodified: now + }; + + return site.getDb().insertRecord(CoreCommentsOfflineProvider.COMMENTS_TABLE, data).then(() => { + return data; + }); + }); + } +} diff --git a/src/core/comments/providers/sync-cron-handler.ts b/src/core/comments/providers/sync-cron-handler.ts new file mode 100644 index 00000000000..5803f936e50 --- /dev/null +++ b/src/core/comments/providers/sync-cron-handler.ts @@ -0,0 +1,48 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreCronHandler } from '@providers/cron'; +import { CoreCommentsSyncProvider } from './sync'; + +/** + * Synchronization cron handler. + */ +@Injectable() +export class CoreCommentsSyncCronHandler implements CoreCronHandler { + name = 'CoreCommentsSyncCronHandler'; + + constructor(private commentsSync: CoreCommentsSyncProvider) {} + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param {string} [siteId] ID of the site affected, undefined for all sites. + * @param {boolean} [force] Wether the execution is forced (manual sync). + * @return {Promise} Promise resolved when done, rejected if failure. + */ + execute(siteId?: string, force?: boolean): Promise { + return this.commentsSync.syncAllComments(siteId, force); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return 300000; // 5 minutes. + } +} diff --git a/src/core/comments/providers/sync.ts b/src/core/comments/providers/sync.ts new file mode 100644 index 00000000000..ab43a6d4b52 --- /dev/null +++ b/src/core/comments/providers/sync.ts @@ -0,0 +1,221 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreAppProvider } from '@providers/app'; +import { CoreCommentsOfflineProvider } from './offline'; +import { CoreCommentsProvider } from './comments'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreSyncProvider } from '@providers/sync'; + +/** + * Service to sync omments. + */ +@Injectable() +export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { + + static AUTO_SYNCED = 'core_comments_autom_synced'; + + constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider, + syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, + private commentsOffline: CoreCommentsOfflineProvider, private utils: CoreUtilsProvider, + private eventsProvider: CoreEventsProvider, private commentsProvider: CoreCommentsProvider, + private coursesProvider: CoreCoursesProvider, timeUtils: CoreTimeUtilsProvider) { + + super('CoreCommentsSync', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils); + } + + /** + * Try to synchronize all the comments in a certain site or in all sites. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @param {boolean} [force] Wether to force sync not depending on last execution. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllComments(siteId?: string, force?: boolean): Promise { + return this.syncOnSites('all comments', this.syncAllCommentsFunc.bind(this), [force], siteId); + } + + /** + * Synchronize all the comments in a certain site + * + * @param {string} siteId Site ID to sync. + * @param {boolean} force Wether to force sync not depending on last execution. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + private syncAllCommentsFunc(siteId: string, force: boolean): Promise { + return this.commentsOffline.getAllComments(siteId).then((comments) => { + // Sync all courses. + const promises = comments.map((comment) => { + const promise = force ? this.syncComment(comment.contextlevel, comment.instanceid, comment.component, + comment.itemid, comment.area, siteId) : this.syncCommentIfNeeded(comment.contextlevel, comment.instanceid, + comment.component, comment.itemid, comment.area, siteId); + + return promise.then((warnings) => { + if (typeof warnings != 'undefined') { + // Sync successful, send event. + this.eventsProvider.trigger(CoreCommentsSyncProvider.AUTO_SYNCED, { + contextLevel: comment.contextlevel, + instanceId: comment.instanceid, + componentName: comment.component, + itemId: comment.itemid, + area: comment.area, + warnings: warnings + }, siteId); + } + }); + }); + + return Promise.all(promises); + }); + } + + /** + * Sync course notes only if a certain time has passed since the last time. + * + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the notes are synced or if they don't need to be synced. + */ + private syncCommentIfNeeded(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + const syncId = this.getSyncId(contextLevel, instanceId, component, itemId, area); + + return this.isSyncNeeded(syncId, siteId).then((needed) => { + if (needed) { + return this.syncComment(contextLevel, instanceId, component, itemId, area, siteId); + } + }); + } + + /** + * Synchronize notes of a course. + * + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + syncComment(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const syncId = this.getSyncId(contextLevel, instanceId, component, itemId, area); + + if (this.isSyncing(syncId, siteId)) { + // There's already a sync ongoing for notes, return the promise. + return this.getOngoingSync(syncId, siteId); + } + + this.logger.debug('Try to sync comments ' + syncId); + + const warnings = []; + + // Get offline comments to be sent. + const syncPromise = this.commentsOffline.getComment(contextLevel, instanceId, component, itemId, area, siteId) + .then((comment) => { + if (!comment) { + // Nothing to sync. + return; + } else if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + return Promise.reject(this.translate.instant('core.networkerrormsg')); + } + + const errors = []; + let commentsResponse = []; + let promise; + + if (comment.action == 'add') { + promise = this.commentsProvider.addCommentOnline(comment.content, contextLevel, instanceId, component, itemId, area, + siteId); + } + + // Send the comments. + return promise.then((response) => { + commentsResponse = response; + + // Fetch the comments from server to be sure they're up to date. + return this.commentsProvider.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId) + .then(() => { + return this.commentsProvider.getComments(contextLevel, instanceId, component, itemId, area, 0, siteId); + }).catch(() => { + // Ignore errors. + }); + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, this means the user cannot send comments. + errors.push(error); + } else { + // Not a WebService error, reject the synchronization to try again. + return Promise.reject(error); + } + }).then(() => { + // Notes were sent, delete them from local DB. + const promises = commentsResponse.map((comment) => { + return this.commentsOffline.removeComment(contextLevel, instanceId, component, itemId, area, siteId); + }); + + return Promise.all(promises); + }).then(() => { + if (errors && errors.length) { + errors.forEach((error) => { + warnings.push(this.translate.instant('addon.notes.warningnotenotsent', { + contextLevel: contextLevel, + instanceId: instanceId, + componentName: component, + itemId: itemId, + area: area, + error: error + })); + }); + } + }); + }).then(() => { + // All done, return the warnings. + return warnings; + }); + + return this.addOngoingSync(syncId, syncPromise, siteId); + } + + /** + * Get the ID of a comments sync. + * + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @return {string} Sync ID. + */ + protected getSyncId(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = ''): string { + return contextLevel + '#' + instanceId + '#' + component + '#' + itemId + '#' + area; + } +} From 8a56dc84b841b1743187f14c2f4a093e21232a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 3 Jul 2019 11:36:23 +0200 Subject: [PATCH 081/241] MOBILE-2877 comments: Delete comments --- src/addon/blog/components/entries/entries.ts | 8 +- src/assets/lang/en.json | 3 + src/core/comments/lang/en.json | 5 +- src/core/comments/pages/add/add.ts | 2 +- src/core/comments/pages/viewer/viewer.html | 19 +- src/core/comments/pages/viewer/viewer.ts | 120 ++++++++++-- src/core/comments/providers/comments.ts | 97 ++++++++-- src/core/comments/providers/offline.ts | 191 +++++++++++++++++-- src/core/comments/providers/sync.ts | 82 ++++---- 9 files changed, 435 insertions(+), 92 deletions(-) diff --git a/src/addon/blog/components/entries/entries.ts b/src/addon/blog/components/entries/entries.ts index d737c8e1bfe..804538d8569 100644 --- a/src/addon/blog/components/entries/entries.ts +++ b/src/addon/blog/components/entries/entries.ts @@ -169,11 +169,13 @@ export class AddonBlogEntriesComponent implements OnInit { * @param {any} refresher Refresher instance. */ refresh(refresher?: any): void { - this.entries.forEach((entry) => { - this.commentsProvider.invalidateCommentsData('user', entry.userid, this.component, entry.id, 'format_blog'); + const promises = this.entries.map((entry) => { + return this.commentsProvider.invalidateCommentsData('user', entry.userid, this.component, entry.id, 'format_blog'); }); - this.blogProvider.invalidateEntries(this.filter).finally(() => { + promises.push(this.blogProvider.invalidateEntries(this.filter)); + + Promise.all(promises).finally(() => { this.fetchEntries(true).finally(() => { if (refresher) { refresher.complete(); diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 4ada12bc37e..5b481b28a05 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1269,9 +1269,12 @@ "core.comments.comments": "Comments", "core.comments.commentscount": "Comments ({{$a}})", "core.comments.commentsnotworking": "Comments cannot be retrieved", + "core.comments.deletecommentbyon": "Delete comment posted by {{$a.user}} on {{$a.time}}", "core.comments.eventcommentcreated": "Comment created", + "core.comments.eventcommentdeleted": "Comment deleted", "core.comments.nocomments": "No comments", "core.comments.savecomment": "Save comment", + "core.comments.warningcommentsnotsent": "Couldn't sync comments. {{error}}", "core.completion-alt-auto-fail": "Completed: {{$a}} (did not achieve pass grade)", "core.completion-alt-auto-n": "Not completed: {{$a}}", "core.completion-alt-auto-n-override": "Not completed: {{$a.modname}} (set by {{$a.overrideuser}})", diff --git a/src/core/comments/lang/en.json b/src/core/comments/lang/en.json index b6c99e4c3cf..c48dcce1716 100644 --- a/src/core/comments/lang/en.json +++ b/src/core/comments/lang/en.json @@ -3,7 +3,10 @@ "comments": "Comments", "commentscount": "Comments ({{$a}})", "commentsnotworking": "Comments cannot be retrieved", + "deletecommentbyon": "Delete comment posted by {{$a.user}} on {{$a.time}}", "eventcommentcreated": "Comment created", + "eventcommentdeleted": "Comment deleted", "nocomments": "No comments", - "savecomment": "Save comment" + "savecomment": "Save comment", + "warningcommentsnotsent": "Couldn't sync comments. {{error}}" } \ No newline at end of file diff --git a/src/core/comments/pages/add/add.ts b/src/core/comments/pages/add/add.ts index 1d58db46f9f..66510fb7979 100644 --- a/src/core/comments/pages/add/add.ts +++ b/src/core/comments/pages/add/add.ts @@ -57,7 +57,7 @@ export class CoreCommentsAddPage { this.appProvider.closeKeyboard(); const loadingModal = this.domUtils.showModalLoading('core.sending', true); - // Freeze the add note button. + // Freeze the add comment button. this.processing = true; this.commentsProvider.addComment(this.content, this.contextLevel, this.instanceId, this.componentName, this.itemId, this.area).then((commentsResponse) => { diff --git a/src/core/comments/pages/viewer/viewer.html b/src/core/comments/pages/viewer/viewer.html index 812838695e2..2a023a41992 100644 --- a/src/core/comments/pages/viewer/viewer.html +++ b/src/core/comments/pages/viewer/viewer.html @@ -2,6 +2,9 @@ + @@ -21,13 +24,16 @@ {{ 'core.thereisdatatosync' | translate:{$a: 'core.comments.comments' | translate | lowercase } }}
- +

{{ offlineComment.fullname }}

{{ 'core.notsent' | translate }}

+
@@ -38,7 +44,16 @@

{{ offlineComment.fullname }}

{{ comment.fullname }}

-

{{ comment.timecreated * 1000 | coreFormatDate: 'strftimerecentfull' }}

+

{{ comment.timecreated * 1000 | coreFormatDate: 'strftimerecentfull' }}

+

+ {{ 'core.deletedoffline' | translate }} +

+ +
diff --git a/src/core/comments/pages/viewer/viewer.ts b/src/core/comments/pages/viewer/viewer.ts index 50bbf004f7d..0f606feee35 100644 --- a/src/core/comments/pages/viewer/viewer.ts +++ b/src/core/comments/pages/viewer/viewer.ts @@ -15,9 +15,11 @@ import { Component, ViewChild, OnDestroy } from '@angular/core'; import { IonicPage, Content, NavParams, ModalController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; +import { coreSlideInOut } from '@classes/animations'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreEventsProvider } from '@providers/events'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreCommentsProvider } from '../../providers/comments'; @@ -31,6 +33,7 @@ import { CoreCommentsSyncProvider } from '../../providers/sync'; @Component({ selector: 'page-core-comments-viewer', templateUrl: 'viewer.html', + animations: [coreSlideInOut] }) export class CoreCommentsViewerPage implements OnDestroy { @ViewChild(Content) content: Content; @@ -47,12 +50,15 @@ export class CoreCommentsViewerPage implements OnDestroy { canLoadMore = false; loadMoreError = false; canAddComments = false; + canDeleteComments = false; + showDelete = false; hasOffline = false; refreshIcon = 'spinner'; syncIcon = 'spinner'; offlineComment: any; + currentUserId: number; - protected addCommentsAvailable = false; + protected addDeleteCommentsAvailable = false; protected syncObserver: any; protected currentUser: any; @@ -60,7 +66,7 @@ export class CoreCommentsViewerPage implements OnDestroy { private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private modalCtrl: ModalController, private commentsProvider: CoreCommentsProvider, private offlineComments: CoreCommentsOfflineProvider, eventsProvider: CoreEventsProvider, private commentsSync: CoreCommentsSyncProvider, - private textUtils: CoreTextUtilsProvider) { + private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider) { this.contextLevel = navParams.get('contextLevel'); this.instanceId = navParams.get('instanceId'); @@ -96,34 +102,35 @@ export class CoreCommentsViewerPage implements OnDestroy { */ ionViewDidLoad(): void { this.commentsProvider.isAddCommentsAvailable().then((enabled) => { - this.addCommentsAvailable = enabled; + // Is implicit the user can delete if he can add. + this.addDeleteCommentsAvailable = enabled; }); + this.currentUserId = this.sitesProvider.getCurrentSiteUserId(); this.fetchComments(true); } /** * Fetches the comments. * - * @param {boolean} sync When to resync notes. + * @param {boolean} sync When to resync comments. * @param {boolean} [showErrors] When to display errors or not. * @return {Promise} Resolved when done. */ protected fetchComments(sync: boolean, showErrors?: boolean): Promise { this.loadMoreError = false; - const promise = sync ? this.syncComment(showErrors) : Promise.resolve(); + const promise = sync ? this.syncComments(showErrors) : Promise.resolve(); return promise.catch(() => { // Ignore errors. }).then(() => { return this.offlineComments.getComment(this.contextLevel, this.instanceId, this.componentName, this.itemId, this.area).then((offlineComment) => { - this.hasOffline = !!offlineComment; this.offlineComment = offlineComment; - if (this.hasOffline && !this.currentUser) { - return this.userProvider.getProfile(this.sitesProvider.getCurrentSiteUserId(), undefined, true).then((user) => { + if (offlineComment && !this.currentUser) { + return this.userProvider.getProfile(this.currentUserId, undefined, true).then((user) => { this.currentUser = user; this.offlineComment.profileimageurl = user.profileimageurl; this.offlineComment.fullname = user.fullname; @@ -131,32 +138,53 @@ export class CoreCommentsViewerPage implements OnDestroy { }).catch(() => { // Ignore errors. }); - } else if (this.hasOffline) { + } else if (offlineComment) { this.offlineComment.profileimageurl = this.currentUser.profileimageurl; this.offlineComment.fullname = this.currentUser.fullname; this.offlineComment.userid = this.currentUser.id; } + + return this.offlineComments.getDeletedComments(this.contextLevel, this.instanceId, this.componentName, this.itemId, + this.area); }); - }).then(() => { + }).then((deletedComments) => { + this.hasOffline = !!this.offlineComment || deletedComments.length > 0; // Get comments data. return this.commentsProvider.getComments(this.contextLevel, this.instanceId, this.componentName, this.itemId, this.area, this.page).then((response) => { - this.canAddComments = this.addCommentsAvailable && response.canpost; + this.canAddComments = this.addDeleteCommentsAvailable && response.canpost; const comments = response.comments.sort((a, b) => b.timecreated - a.timecreated); this.canLoadMore = comments.length >= CoreCommentsProvider.pageSize; - this.comments.forEach((comment) => { + return Promise.all(comments.map((comment) => { // Get the user profile image. - this.userProvider.getProfile(comment.userid, undefined, true).then((user) => { + return this.userProvider.getProfile(comment.userid, undefined, true).then((user) => { comment.profileimageurl = user.profileimageurl; + + return comment; }).catch(() => { // Ignore errors. + return comment; }); + })); + }).then((comments) => { + this.comments = this.comments.concat(comments); + + deletedComments && deletedComments.forEach((deletedComment) => { + const comment = this.comments.find((comment) => { + return comment.id == deletedComment.commentid; + }); + + if (comment) { + comment.deleted = deletedComment.deleted; + } }); - this.comments = this.comments.concat(comments); + this.canDeleteComments = this.addDeleteCommentsAvailable && (this.hasOffline || this.comments.some((comment) => { + return !!comment.delete; + })); }); }).catch((error) => { this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. @@ -174,7 +202,7 @@ export class CoreCommentsViewerPage implements OnDestroy { } /** - * Function to load more cp,,emts. + * Function to load more commemts. * * @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading. * @return {Promise} Resolved when done. @@ -228,8 +256,8 @@ export class CoreCommentsViewerPage implements OnDestroy { * @param {boolean} showErrors Whether to display errors or not. * @return {Promise} Promise resolved if sync is successful, rejected otherwise. */ - private syncComment(showErrors: boolean): Promise { - return this.commentsSync.syncComment(this.contextLevel, this.instanceId, this.componentName, this.itemId, + private syncComments(showErrors: boolean): Promise { + return this.commentsSync.syncComments(this.contextLevel, this.instanceId, this.componentName, this.itemId, this.area).then((warnings) => { this.showSyncWarnings(warnings); }).catch((error) => { @@ -263,6 +291,7 @@ export class CoreCommentsViewerPage implements OnDestroy { modal.onDidDismiss((data) => { if (data && data.comments) { this.comments = data.comments.concat(this.comments); + this.canDeleteComments = this.addDeleteCommentsAvailable; } else if (data && !data.comments) { this.fetchComments(false); } @@ -270,6 +299,63 @@ export class CoreCommentsViewerPage implements OnDestroy { modal.present(); } + /** + * Delete a comment. + * + * @param {Event} e Click event. + * @param {any} comment Comment to delete. + */ + deleteComment(e: Event, comment: any): void { + e.preventDefault(); + e.stopPropagation(); + + const time = this.timeUtils.userDate((comment.lastmodified || comment.timecreated) * 1000, 'core.strftimerecentfull'); + + comment.contextlevel = this.contextLevel; + comment.instanceid = this.instanceId; + comment.component = this.componentName; + comment.itemid = this.itemId; + comment.area = this.area; + + this.domUtils.showConfirm(this.translate.instant('core.comments.deletecommentbyon', {$a: + { user: comment.fullname || '', time: time } })).then(() => { + this.commentsProvider.deleteComment(comment).then(() => { + this.showDelete = false; + + this.refreshComments(true); + + this.domUtils.showToast('core.comments.eventcommentdeleted', true, 3000); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Delete comment failed.'); + }); + }).catch(() => { + // User cancelled, nothing to do. + }); + } + + /** + * Restore a comment. + * + * @param {Event} e Click event. + * @param {any} comment Comment to delete. + */ + undoDeleteComment(e: Event, comment: any): void { + e.preventDefault(); + e.stopPropagation(); + + this.offlineComments.undoDeleteComment(comment.id).then(() => { + comment.deleted = false; + this.showDelete = false; + }); + } + + /** + * Toggle delete. + */ + toggleDelete(): void { + this.showDelete = !this.showDelete; + } + /** * Page destroyed. */ diff --git a/src/core/comments/providers/comments.ts b/src/core/comments/providers/comments.ts index a17748b08c3..eb5613c4584 100644 --- a/src/core/comments/providers/comments.ts +++ b/src/core/comments/providers/comments.ts @@ -48,7 +48,7 @@ export class CoreCommentsProvider { siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - // Convenience function to store a note to be synchronized later. + // Convenience function to store a comment to be synchronized later. const storeOffline = (): Promise => { return this.commentsOffline.saveComment(content, contextLevel, instanceId, component, itemId, area, siteId).then(() => { return Promise.resolve(false); @@ -56,11 +56,11 @@ export class CoreCommentsProvider { }; if (!this.appProvider.isOnline()) { - // App is offline, store the note. + // App is offline, store the comment. return storeOffline(); } - // Send note to server. + // Send comment to server. return this.addCommentOnline(content, contextLevel, instanceId, component, itemId, area, siteId).then((comments) => { return comments; }).catch((error) => { @@ -69,7 +69,7 @@ export class CoreCommentsProvider { return Promise.reject(error); } - // Error sending note, store it to retry later. + // Error sending comment, store it to retry later. return storeOffline(); }); } @@ -115,7 +115,7 @@ export class CoreCommentsProvider { * @param {any[]} comments Comments to save. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when added, rejected otherwise. Promise resolved doesn't mean that comments - * have been added, the resolve param can contain errors for notes not sent. + * have been added, the resolve param can contain errors for comments not sent. */ addCommentsOnline(comments: any[], siteId?: string): Promise { if (!comments || !comments.length) { @@ -155,6 +155,79 @@ export class CoreCommentsProvider { }); } + /** + * Delete a comment. + * + * @param {any} comment Comment object to delete. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that comments + * have been deleted, the resolve param can contain errors for comments not deleted. + */ + deleteComment(comment: any, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (!comment.id) { + return this.commentsOffline.removeComment(comment.contextlevel, comment.instanceid, comment.component, comment.itemid, + comment.area, siteId); + } + + // Convenience function to store the action to be synchronized later. + const storeOffline = (): Promise => { + return this.commentsOffline.deleteComment(comment.id, comment.contextlevel, comment.instanceid, comment.component, + comment.itemid, comment.area, siteId).then(() => { + return false; + }); + }; + + if (!this.appProvider.isOnline()) { + // App is offline, store the comment. + return storeOffline(); + } + + // Send comment to server. + return this.deleteCommentsOnline([comment.id], comment.contextlevel, comment.instanceid, comment.component, comment.itemid, + comment.area, siteId).then(() => { + return true; + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, the user cannot send the comment so don't store it. + return Promise.reject(error); + } + + // Error sending comment, store it to retry later. + return storeOffline(); + }); + } + + /** + * Delete a comment. It will fail if offline or cannot connect. + * + * @param {number[]} commentIds Comment IDs to delete. + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that comments + * have been deleted, the resolve param can contain errors for comments not deleted. + */ + deleteCommentsOnline(commentIds: number[], contextLevel: string, instanceId: number, component: string, itemId: number, + area: string = '', siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const data = { + comments: commentIds + }; + + return site.write('core_comment_delete_comments', data).then((response) => { + // A comment was deleted, invalidate comments. + return this.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId).catch(() => { + // Ignore errors. + }); + }); + }); + } + /** * Returns whether WS to add/delete comments are available in site. * @@ -239,7 +312,7 @@ export class CoreCommentsProvider { } /** - * Get comments count number to show ont he comments component. + * Get comments count number to show on the comments component. * * @param {string} contextLevel Contextlevel system, course, user... * @param {number} instanceId The Instance id of item associated with the context level. @@ -284,7 +357,6 @@ export class CoreCommentsProvider { return getCommentsPageCount(1).then((countMore) => { // Page limit was reached on the previous call. if (countMore > 0) { - CoreCommentsProvider.pageSizeOK = true; return (CoreCommentsProvider.pageSize - 1) + '+'; } @@ -308,11 +380,14 @@ export class CoreCommentsProvider { invalidateCommentsData(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - // This is done with starting with to avoid conflicts with previous keys that were including page. - site.invalidateWsCacheForKeyStartingWith(this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, - area) + ':'); - return site.invalidateWsCacheForKey(this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, area)); + return this.utils.allPromises([ + // This is done with starting with to avoid conflicts with previous keys that were including page. + site.invalidateWsCacheForKeyStartingWith(this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, + area) + ':'), + + site.invalidateWsCacheForKey(this.getCommentsCacheKey(contextLevel, instanceId, component, itemId, area)) + ]); }); } diff --git a/src/core/comments/providers/offline.ts b/src/core/comments/providers/offline.ts index 82dbd689472..94caf9fb336 100644 --- a/src/core/comments/providers/offline.ts +++ b/src/core/comments/providers/offline.ts @@ -24,6 +24,7 @@ export class CoreCommentsOfflineProvider { // Variables for database. static COMMENTS_TABLE = 'core_comments_offline_comments'; + static COMMENTS_DELETED_TABLE = 'core_comments_deleted_offline_comments'; protected siteSchema: CoreSiteSchema = { name: 'CoreCommentsOfflineProvider', version: 1, @@ -55,16 +56,46 @@ export class CoreCommentsOfflineProvider { name: 'content', type: 'TEXT' }, - { - name: 'action', - type: 'TEXT' - }, { name: 'lastmodified', type: 'INTEGER' } ], primaryKeys: ['contextlevel', 'instanceid', 'component', 'itemid', 'area'] + }, + { + name: CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE, + columns: [ + { + name: 'commentid', + type: 'INTEGER', + primaryKey: true + }, + { + name: 'contextlevel', + type: 'TEXT' + }, + { + name: 'instanceid', + type: 'INTEGER' + }, + { + name: 'component', + type: 'TEXT', + }, + { + name: 'itemid', + type: 'INTEGER' + }, + { + name: 'area', + type: 'TEXT' + }, + { + name: 'deleted', + type: 'INTEGER' + } + ] } ] }; @@ -74,7 +105,22 @@ export class CoreCommentsOfflineProvider { } /** - * Delete a comment. + * Get all offline comments. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with comments. + */ + getAllComments(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return Promise.all([site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_TABLE), + site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE)]).then((results) => { + return [].concat.apply([], results); + }); + }); + } + + /** + * Get an offline comment. * * @param {string} contextLevel Contextlevel system, course, user... * @param {number} instanceId The Instance id of item associated with the context level. @@ -82,30 +128,60 @@ export class CoreCommentsOfflineProvider { * @param {number} itemId Associated id. * @param {string} [area=''] String comment area. Default empty. * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved if deleted, rejected if failure. + * @return {Promise} Promise resolved with the comments. */ - removeComment(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + getComment(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().deleteRecords(CoreCommentsOfflineProvider.COMMENTS_TABLE, { + return site.getDb().getRecord(CoreCommentsOfflineProvider.COMMENTS_TABLE, { contextlevel: contextLevel, instanceid: instanceId, component: component, itemid: itemId, area: area }); + }).catch(() => { + return false; }); } /** - * Get all offline comments. + * Get all offline comments added or deleted of a special area. + * + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the comments. + */ + getComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + let comments = []; + + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.getComment(contextLevel, instanceId, component, itemId, area, siteId).then((comment) => { + comments = comment ? [comment] : []; + + return this.getDeletedComments(contextLevel, instanceId, component, itemId, area, siteId); + }).then((deletedComments) => { + comments = comments.concat(deletedComments); + + return comments; + }); + } + + /** + * Get all offline deleted comments. * * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with comments. */ - getAllComments(siteId?: string): Promise { + getAllDeletedComments(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_TABLE); + return site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE); }); } @@ -120,10 +196,10 @@ export class CoreCommentsOfflineProvider { * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the comments. */ - getComment(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + getDeletedComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getRecord(CoreCommentsOfflineProvider.COMMENTS_TABLE, { + return site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE, { contextlevel: contextLevel, instanceid: instanceId, component: component, @@ -136,19 +212,50 @@ export class CoreCommentsOfflineProvider { } /** - * Check if there are offline comments. + * Remove an offline comment. * * @param {string} contextLevel Contextlevel system, course, user... * @param {number} instanceId The Instance id of item associated with the context level. * @param {string} component Component name. * @param {number} itemId Associated id. * @param {string} [area=''] String comment area. Default empty. - * @return {Promise} Promise resolved with boolean: true if has offline comments, false otherwise. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. */ - hasComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', - siteId?: string): Promise { - return this.getComment(contextLevel, instanceId, component, itemId, area, siteId).then((comments) => { - return !!comments.length; + removeComment(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().deleteRecords(CoreCommentsOfflineProvider.COMMENTS_TABLE, { + contextlevel: contextLevel, + instanceid: instanceId, + component: component, + itemid: itemId, + area: area + }); + }); + } + + /** + * Remove an offline deleted comment. + * + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + removeDeletedComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().deleteRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE, { + contextlevel: contextLevel, + instanceid: instanceId, + component: component, + itemid: itemId, + area: area + }); }); } @@ -175,7 +282,6 @@ export class CoreCommentsOfflineProvider { itemid: itemId, area: area, content: content, - action: 'add', lastmodified: now }; @@ -184,4 +290,49 @@ export class CoreCommentsOfflineProvider { }); }); } + + /** + * Delete a comment offline to be sent later. + * + * @param {number} commentId Comment ID. + * @param {string} contextLevel Contextlevel system, course, user... + * @param {number} instanceId The Instance id of item associated with the context level. + * @param {string} component Component name. + * @param {number} itemId Associated id. + * @param {string} [area=''] String comment area. Default empty. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + deleteComment(commentId: number, contextLevel: string, instanceId: number, component: string, itemId: number, + area: string = '', siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const now = this.timeUtils.timestamp(); + const data = { + contextlevel: contextLevel, + instanceid: instanceId, + component: component, + itemid: itemId, + area: area, + commentid: commentId, + deleted: now + }; + + return site.getDb().insertRecord(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE, data).then(() => { + return data; + }); + }); + } + + /** + * Undo delete a comment. + * + * @param {number} commentId Comment ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + undoDeleteComment(commentId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().deleteRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE, { commentid: commentId }); + }); + } } diff --git a/src/core/comments/providers/sync.ts b/src/core/comments/providers/sync.ts index ab43a6d4b52..c8466cac708 100644 --- a/src/core/comments/providers/sync.ts +++ b/src/core/comments/providers/sync.ts @@ -19,7 +19,6 @@ import { CoreSyncBaseProvider } from '@classes/base-sync'; import { CoreAppProvider } from '@providers/app'; import { CoreCommentsOfflineProvider } from './offline'; import { CoreCommentsProvider } from './comments'; -import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreEventsProvider } from '@providers/events'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -39,7 +38,7 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, private commentsOffline: CoreCommentsOfflineProvider, private utils: CoreUtilsProvider, private eventsProvider: CoreEventsProvider, private commentsProvider: CoreCommentsProvider, - private coursesProvider: CoreCoursesProvider, timeUtils: CoreTimeUtilsProvider) { + timeUtils: CoreTimeUtilsProvider) { super('CoreCommentsSync', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils); } @@ -64,10 +63,19 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { */ private syncAllCommentsFunc(siteId: string, force: boolean): Promise { return this.commentsOffline.getAllComments(siteId).then((comments) => { + + // Get Unique array. + comments.forEach((comment) => { + comment.syncId = this.getSyncId(comment.contextlevel, comment.instanceid, comment.component, comment.itemid, + comment.area); + }); + + comments = this.utils.uniqueArray(comments, 'syncId'); + // Sync all courses. const promises = comments.map((comment) => { - const promise = force ? this.syncComment(comment.contextlevel, comment.instanceid, comment.component, - comment.itemid, comment.area, siteId) : this.syncCommentIfNeeded(comment.contextlevel, comment.instanceid, + const promise = force ? this.syncComments(comment.contextlevel, comment.instanceid, comment.component, + comment.itemid, comment.area, siteId) : this.syncCommentsIfNeeded(comment.contextlevel, comment.instanceid, comment.component, comment.itemid, comment.area, siteId); return promise.then((warnings) => { @@ -90,7 +98,7 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { } /** - * Sync course notes only if a certain time has passed since the last time. + * Sync course comments only if a certain time has passed since the last time. * * @param {string} contextLevel Contextlevel system, course, user... * @param {number} instanceId The Instance id of item associated with the context level. @@ -98,21 +106,21 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { * @param {number} itemId Associated id. * @param {string} [area=''] String comment area. Default empty. * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved when the notes are synced or if they don't need to be synced. + * @return {Promise} Promise resolved when the comments are synced or if they don't need to be synced. */ - private syncCommentIfNeeded(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + private syncCommentsIfNeeded(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', siteId?: string): Promise { const syncId = this.getSyncId(contextLevel, instanceId, component, itemId, area); return this.isSyncNeeded(syncId, siteId).then((needed) => { if (needed) { - return this.syncComment(contextLevel, instanceId, component, itemId, area, siteId); + return this.syncComments(contextLevel, instanceId, component, itemId, area, siteId); } }); } /** - * Synchronize notes of a course. + * Synchronize comments in a particular area. * * @param {string} contextLevel Contextlevel system, course, user... * @param {number} instanceId The Instance id of item associated with the context level. @@ -122,14 +130,14 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved if sync is successful, rejected otherwise. */ - syncComment(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + syncComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); const syncId = this.getSyncId(contextLevel, instanceId, component, itemId, area); if (this.isSyncing(syncId, siteId)) { - // There's already a sync ongoing for notes, return the promise. + // There's already a sync ongoing for comments, return the promise. return this.getOngoingSync(syncId, siteId); } @@ -138,9 +146,9 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { const warnings = []; // Get offline comments to be sent. - const syncPromise = this.commentsOffline.getComment(contextLevel, instanceId, component, itemId, area, siteId) - .then((comment) => { - if (!comment) { + const syncPromise = this.commentsOffline.getComments(contextLevel, instanceId, component, itemId, area, siteId) + .then((comments) => { + if (!comments.length) { // Nothing to sync. return; } else if (!this.appProvider.isOnline()) { @@ -148,19 +156,31 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { return Promise.reject(this.translate.instant('core.networkerrormsg')); } - const errors = []; - let commentsResponse = []; - let promise; + const errors = [], + promises = [], + deleteCommentIds = []; + + comments.forEach((comment) => { + if (comment.commentid) { + deleteCommentIds.push(comment.commentid); + } else { + promises.push(this.commentsProvider.addCommentOnline(comment.content, contextLevel, instanceId, component, + itemId, area, siteId).then((response) => { + return this.commentsOffline.removeComment(contextLevel, instanceId, component, itemId, area, siteId); + })); + } + }); - if (comment.action == 'add') { - promise = this.commentsProvider.addCommentOnline(comment.content, contextLevel, instanceId, component, itemId, area, - siteId); + if (deleteCommentIds.length > 0) { + promises.push(this.commentsProvider.deleteCommentsOnline(deleteCommentIds, contextLevel, instanceId, component, + itemId, area, siteId).then((response) => { + return this.commentsOffline.removeDeletedComments(contextLevel, instanceId, component, itemId, area, + siteId); + })); } // Send the comments. - return promise.then((response) => { - commentsResponse = response; - + return Promise.all(promises).then(() => { // Fetch the comments from server to be sure they're up to date. return this.commentsProvider.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId) .then(() => { @@ -171,27 +191,15 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { }).catch((error) => { if (this.utils.isWebServiceError(error)) { // It's a WebService error, this means the user cannot send comments. - errors.push(error); + errors.push(error.message); } else { // Not a WebService error, reject the synchronization to try again. return Promise.reject(error); } - }).then(() => { - // Notes were sent, delete them from local DB. - const promises = commentsResponse.map((comment) => { - return this.commentsOffline.removeComment(contextLevel, instanceId, component, itemId, area, siteId); - }); - - return Promise.all(promises); }).then(() => { if (errors && errors.length) { errors.forEach((error) => { - warnings.push(this.translate.instant('addon.notes.warningnotenotsent', { - contextLevel: contextLevel, - instanceId: instanceId, - componentName: component, - itemId: itemId, - area: area, + warnings.push(this.translate.instant('core.comments.warningcommentsnotsent', { error: error })); }); From a983d1c14b8f77d8bae01f2dd8162f97018efcfe Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 19 Jul 2019 11:45:12 +0200 Subject: [PATCH 082/241] MOBILE-3106 core: Use GET as fallback in get public config --- src/classes/site.ts | 19 +++++++++++++++++-- src/providers/ws.ts | 36 +++++++++++++++++++++++++++--------- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/classes/site.ts b/src/classes/site.ts index 9253f83e631..47a64ffe857 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -21,7 +21,7 @@ import { CoreDbProvider } from '@providers/db'; import { CoreEventsProvider } from '@providers/events'; import { CoreFileProvider } from '@providers/file'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreWSProvider, CoreWSPreSets, CoreWSFileUploadOptions } from '@providers/ws'; +import { CoreWSProvider, CoreWSPreSets, CoreWSFileUploadOptions, CoreWSAjaxPreSets } from '@providers/ws'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -1432,7 +1432,22 @@ export class CoreSite { * @return {Promise} Promise resolved with public config. Rejected with an object if error, see CoreWSProvider.callAjax. */ getPublicConfig(): Promise { - return this.wsProvider.callAjax('tool_mobile_get_public_config', {}, { siteUrl: this.siteUrl }).then((config) => { + const preSets: CoreWSAjaxPreSets = { + siteUrl: this.siteUrl + }; + + return this.wsProvider.callAjax('tool_mobile_get_public_config', {}, preSets).catch((error) => { + + if ((!this.getInfo() || this.isVersionGreaterEqualThan('3.8')) && error && error.errorcode == 'codingerror') { + // This error probably means that there is a redirect in the site. Try to use a GET request. + preSets.noLogin = true; + preSets.useGet = true; + + return this.wsProvider.callAjax('tool_mobile_get_public_config', {}, preSets); + } + + return Promise.reject(error); + }).then((config) => { // Use the wwwroot returned by the server. if (config.httpswwwroot) { this.siteUrl = config.httpswwwroot; diff --git a/src/providers/ws.ts b/src/providers/ws.ts index 0cc906f5abc..08d53055cd3 100644 --- a/src/providers/ws.ts +++ b/src/providers/ws.ts @@ -76,6 +76,18 @@ export interface CoreWSAjaxPreSets { * @type {boolean} */ responseExpected?: boolean; + + /** + * Whether to use the no-login endpoint instead of the normal one. Use it for requests that don't require authentication. + * @type {boolean} + */ + noLogin?: boolean; + + /** + * Whether to send the parameters via GET. Only if noLogin is true. + * @type {boolean} + */ + useGet?: boolean; } /** @@ -215,8 +227,7 @@ export class CoreWSProvider { * - available: 0 if unknown, 1 if available, -1 if not available. */ callAjax(method: string, data: any, preSets: CoreWSAjaxPreSets): Promise { - let siteUrl, - ajaxData; + let promise; if (typeof preSets.siteUrl == 'undefined') { return rejectWithError(this.createFakeWSError('core.unexpectederror', true)); @@ -228,17 +239,24 @@ export class CoreWSProvider { preSets.responseExpected = true; } - ajaxData = [{ - index: 0, - methodname: method, - args: this.convertValuesToString(data) - }]; + const script = preSets.noLogin ? 'service-nologin.php' : 'service.php', + ajaxData = JSON.stringify([{ + index: 0, + methodname: method, + args: this.convertValuesToString(data) + }]); // The info= parameter has no function. It is just to help with debugging. // We call it info to match the parameter name use by Moodle's AMD ajax module. - siteUrl = preSets.siteUrl + '/lib/ajax/service.php?info=' + method; + let siteUrl = preSets.siteUrl + '/lib/ajax/' + script + '?info=' + method; - const promise = this.http.post(siteUrl, JSON.stringify(ajaxData)).timeout(CoreConstants.WS_TIMEOUT).toPromise(); + if (preSets.noLogin && preSets.useGet) { + // Send params using GET. + siteUrl += '&args=' + encodeURIComponent(ajaxData); + promise = this.http.get(siteUrl).timeout(CoreConstants.WS_TIMEOUT).toPromise(); + } else { + promise = this.http.post(siteUrl, ajaxData).timeout(CoreConstants.WS_TIMEOUT).toPromise(); + } return promise.then((data: any) => { // Some moodle web services return null. From f5cfda53a59eec1e429686c96c5e091506f86094 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 11:05:15 +0200 Subject: [PATCH 083/241] MOBILE-2201 tag: List component --- scripts/langindex.json | 1 + src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + src/core/tag/components/components.module.ts | 40 +++++++++++ .../tag/components/list/core-tag-list.html | 3 + src/core/tag/components/list/list.scss | 7 ++ src/core/tag/components/list/list.ts | 45 ++++++++++++ src/core/tag/lang/en.json | 3 + src/core/tag/providers/tag.ts | 70 +++++++++++++++++++ src/core/tag/tag.module.ts | 28 ++++++++ 10 files changed, 200 insertions(+) create mode 100644 src/core/tag/components/components.module.ts create mode 100644 src/core/tag/components/list/core-tag-list.html create mode 100644 src/core/tag/components/list/list.scss create mode 100644 src/core/tag/components/list/list.ts create mode 100644 src/core/tag/lang/en.json create mode 100644 src/core/tag/providers/tag.ts create mode 100644 src/core/tag/tag.module.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 5351ef509a3..89bf47b41f1 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1793,6 +1793,7 @@ "core.submit": "moodle", "core.success": "moodle", "core.tablet": "local_moodlemobileapp", + "core.tag.tags": "moodle", "core.teachers": "moodle", "core.thereisdatatosync": "local_moodlemobileapp", "core.thisdirection": "langconfig", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6969e01bc1b..f908025dea7 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -81,6 +81,7 @@ import { CoreQuestionModule } from '@core/question/question.module'; import { CoreCommentsModule } from '@core/comments/comments.module'; import { CoreBlockModule } from '@core/block/block.module'; import { CoreRatingModule } from '@core/rating/rating.module'; +import { CoreTagModule } from '@core/tag/tag.module'; // Addon modules. import { AddonBadgesModule } from '@addon/badges/badges.module'; @@ -223,6 +224,7 @@ export const CORE_PROVIDERS: any[] = [ CoreBlockModule, CoreRatingModule, CorePushNotificationsModule, + CoreTagModule, AddonBadgesModule, AddonBlogModule, AddonCalendarModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 21810db9524..0dc92ff1336 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1793,6 +1793,7 @@ "core.submit": "Submit", "core.success": "Success", "core.tablet": "Tablet", + "core.tag.tags": "Tags", "core.teachers": "Teachers", "core.thereisdatatosync": "There are offline {{$a}} to be synchronised.", "core.thisdirection": "ltr", diff --git a/src/core/tag/components/components.module.ts b/src/core/tag/components/components.module.ts new file mode 100644 index 00000000000..c2e07f85bfa --- /dev/null +++ b/src/core/tag/components/components.module.ts @@ -0,0 +1,40 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreTagListComponent } from './list/list'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + CoreTagListComponent + ], + imports: [ + CommonModule, + IonicModule, + CoreDirectivesModule, + TranslateModule.forChild() + ], + providers: [ + ], + exports: [ + CoreTagListComponent + ], + entryComponents: [ + ] +}) +export class CoreTagComponentsModule {} diff --git a/src/core/tag/components/list/core-tag-list.html b/src/core/tag/components/list/core-tag-list.html new file mode 100644 index 00000000000..7e6372e20eb --- /dev/null +++ b/src/core/tag/components/list/core-tag-list.html @@ -0,0 +1,3 @@ + + {{ tag.rawname }} + diff --git a/src/core/tag/components/list/list.scss b/src/core/tag/components/list/list.scss new file mode 100644 index 00000000000..569d645d627 --- /dev/null +++ b/src/core/tag/components/list/list.scss @@ -0,0 +1,7 @@ +ion-app.app-root core-tag-list { + line-height: 1.6; + + ion-badge { + cursor: pointer; + } +} diff --git a/src/core/tag/components/list/list.ts b/src/core/tag/components/list/list.ts new file mode 100644 index 00000000000..6abbf3d7fad --- /dev/null +++ b/src/core/tag/components/list/list.ts @@ -0,0 +1,45 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Input, Optional } from '@angular/core'; +import { NavController } from 'ionic-angular'; +import { CoreTagItem } from '@core/tag/providers/tag'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; + +/** + * Component that displays the list of tags of an item. + */ +@Component({ + selector: 'core-tag-list', + templateUrl: 'core-tag-list.html' +}) +export class CoreTagListComponent { + @Input() tags: CoreTagItem[]; + + constructor(private navCtrl: NavController, @Optional() private svComponent: CoreSplitViewComponent) {} + + /** + * Go to tag index page. + */ + openTag(tag: CoreTagItem): void { + const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; + const params = { + tagId: tag.id, + tagName: tag.rawname, + collectionId: tag.tagcollid, + fromContextId: tag.taginstancecontextid + }; + navCtrl.push('CoreTagIndexPage', params); + } +} diff --git a/src/core/tag/lang/en.json b/src/core/tag/lang/en.json new file mode 100644 index 00000000000..fec56bb365c --- /dev/null +++ b/src/core/tag/lang/en.json @@ -0,0 +1,3 @@ +{ + "tags": "Tags" +} diff --git a/src/core/tag/providers/tag.ts b/src/core/tag/providers/tag.ts new file mode 100644 index 00000000000..fdcdaf189a2 --- /dev/null +++ b/src/core/tag/providers/tag.ts @@ -0,0 +1,70 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreSitesProvider } from '@providers/sites'; +import { CoreSite } from '@classes/site'; + +/** + * Structure of a tag item returned by WS. + */ +export interface CoreTagItem { + id: number; + name: string; + rawname: string; + isstandard: boolean; + tagcollid: number; + taginstanceid: number; + taginstancecontextid: number; + itemid: number; + ordering: number; + flag: number; +} + +/** + * Service to handle tags. + */ +@Injectable() +export class CoreTagProvider { + + constructor(private sitesProvider: CoreSitesProvider) {} + + /** + * Check whether tags are available in a certain site. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if available, resolved with false otherwise. + * @since 3.7 + */ + areTagsAvailable(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.areTagsAvailableInSite(site); + }); + } + + /** + * Check whether tags are available in a certain site. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} True if available. + */ + areTagsAvailableInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.wsAvailable('core_tag_get_tagindex_per_area') && + site.wsAvailable('core_tag_get_tag_cloud') && + site.wsAvailable('core_tag_get_tag_collections') && + !site.isFeatureDisabled('NoDelegate_CoreTag'); + } +} diff --git a/src/core/tag/tag.module.ts b/src/core/tag/tag.module.ts new file mode 100644 index 00000000000..eaa2f9e81b8 --- /dev/null +++ b/src/core/tag/tag.module.ts @@ -0,0 +1,28 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreTagProvider } from './providers/tag'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + providers: [ + CoreTagProvider, + ] +}) +export class CoreTagModule { +} From 2ea97b0840beb4b0d27304863e20f67829a2d002 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 11:13:50 +0200 Subject: [PATCH 084/241] MOBILE-2201 blog: Display tags in blog posts --- src/addon/blog/components/components.module.ts | 4 +++- src/addon/blog/components/entries/addon-blog-entries.html | 4 ++++ src/addon/blog/components/entries/entries.ts | 5 ++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/addon/blog/components/components.module.ts b/src/addon/blog/components/components.module.ts index 0e56fcc3fda..efba08d7d7b 100644 --- a/src/addon/blog/components/components.module.ts +++ b/src/addon/blog/components/components.module.ts @@ -20,6 +20,7 @@ import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; import { CorePipesModule } from '@pipes/pipes.module'; import { CoreCommentsComponentsModule } from '@core/comments/components/components.module'; +import { CoreTagComponentsModule } from '@core/tag/components/components.module'; import { AddonBlogEntriesComponent } from './entries/entries'; @NgModule({ @@ -33,7 +34,8 @@ import { AddonBlogEntriesComponent } from './entries/entries'; CoreComponentsModule, CoreDirectivesModule, CorePipesModule, - CoreCommentsComponentsModule + CoreCommentsComponentsModule, + CoreTagComponentsModule ], providers: [ ], diff --git a/src/addon/blog/components/entries/addon-blog-entries.html b/src/addon/blog/components/entries/addon-blog-entries.html index 670e4d276c1..690930091c4 100644 --- a/src/addon/blog/components/entries/addon-blog-entries.html +++ b/src/addon/blog/components/entries/addon-blog-entries.html @@ -29,6 +29,10 @@

+ +
{{ 'core.tag.tags' | translate }}:
+ +
diff --git a/src/addon/blog/components/entries/entries.ts b/src/addon/blog/components/entries/entries.ts index b66db02a32b..a0e4bd3da47 100644 --- a/src/addon/blog/components/entries/entries.ts +++ b/src/addon/blog/components/entries/entries.ts @@ -19,6 +19,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreUserProvider } from '@core/user/providers/user'; import { AddonBlogProvider } from '../../providers/blog'; import { CoreCommentsProvider } from '@core/comments/providers/comments'; +import { CoreTagProvider } from '@core/tag/providers/tag'; /** * Component that displays the blog entries. @@ -49,10 +50,11 @@ export class AddonBlogEntriesComponent implements OnInit { onlyMyEntries = false; component = AddonBlogProvider.COMPONENT; commentsEnabled: boolean; + tagsEnabled: boolean; constructor(protected blogProvider: AddonBlogProvider, protected domUtils: CoreDomUtilsProvider, protected userProvider: CoreUserProvider, sitesProvider: CoreSitesProvider, - protected commentsProvider: CoreCommentsProvider) { + protected commentsProvider: CoreCommentsProvider, private tagProvider: CoreTagProvider) { this.currentUserId = sitesProvider.getCurrentSiteUserId(); } @@ -85,6 +87,7 @@ export class AddonBlogEntriesComponent implements OnInit { } this.commentsEnabled = !this.commentsProvider.areCommentsDisabledInSite(); + this.tagsEnabled = this.tagProvider.areTagsAvailableInSite(); this.fetchEntries().then(() => { this.blogProvider.logView(this.filter).catch(() => { From 85d214edba9d420198217354f99546a08034736f Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 15:09:36 +0200 Subject: [PATCH 085/241] MOBILE-2201 book: Display tags in book chapters --- src/addon/mod/book/components/components.module.ts | 4 +++- .../book/components/index/addon-mod-book-index.html | 4 ++++ src/addon/mod/book/components/index/index.ts | 6 +++++- src/addon/mod/book/providers/book.ts | 12 ++++++++++-- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/addon/mod/book/components/components.module.ts b/src/addon/mod/book/components/components.module.ts index 54e83ef50dd..4cc338b6eeb 100644 --- a/src/addon/mod/book/components/components.module.ts +++ b/src/addon/mod/book/components/components.module.ts @@ -20,6 +20,7 @@ import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreCourseComponentsModule } from '@core/course/components/components.module'; import { AddonModBookIndexComponent } from './index/index'; +import { CoreTagComponentsModule } from '@core/tag/components/components.module'; @NgModule({ declarations: [ @@ -31,7 +32,8 @@ import { AddonModBookIndexComponent } from './index/index'; TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, - CoreCourseComponentsModule + CoreCourseComponentsModule, + CoreTagComponentsModule ], providers: [ ], diff --git a/src/addon/mod/book/components/index/addon-mod-book-index.html b/src/addon/mod/book/components/index/addon-mod-book-index.html index 6f4e0be3ab0..13cc19b7e3e 100644 --- a/src/addon/mod/book/components/index/addon-mod-book-index.html +++ b/src/addon/mod/book/components/index/addon-mod-book-index.html @@ -21,6 +21,10 @@
+
+ {{ 'core.tag.tags' | translate }}: + +
diff --git a/src/addon/mod/book/components/index/index.ts b/src/addon/mod/book/components/index/index.ts index 1b154d37392..569060aa335 100644 --- a/src/addon/mod/book/components/index/index.ts +++ b/src/addon/mod/book/components/index/index.ts @@ -19,6 +19,7 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; import { AddonModBookProvider, AddonModBookContentsMap, AddonModBookTocChapter } from '../../providers/book'; import { AddonModBookPrefetchHandler } from '../../providers/prefetch-handler'; +import { CoreTagProvider } from '@core/tag/providers/tag'; /** * Component that displays a book. @@ -34,6 +35,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp chapterContent: string; previousChapter: string; nextChapter: string; + tagsEnabled: boolean; protected chapters: AddonModBookTocChapter[]; protected currentChapter: string; @@ -41,7 +43,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp constructor(injector: Injector, private bookProvider: AddonModBookProvider, private courseProvider: CoreCourseProvider, private appProvider: CoreAppProvider, private prefetchDelegate: AddonModBookPrefetchHandler, - private modalCtrl: ModalController, @Optional() private content: Content) { + private modalCtrl: ModalController, private tagProvider: CoreTagProvider, @Optional() private content: Content) { super(injector); } @@ -51,6 +53,8 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp ngOnInit(): void { super.ngOnInit(); + this.tagsEnabled = this.tagProvider.areTagsAvailableInSite(); + this.loadContent(); } diff --git a/src/addon/mod/book/providers/book.ts b/src/addon/mod/book/providers/book.ts index 1655ed164a1..74fd32706ba 100644 --- a/src/addon/mod/book/providers/book.ts +++ b/src/addon/mod/book/providers/book.ts @@ -24,6 +24,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreSite } from '@classes/site'; +import { CoreTagItem } from '@core/tag/providers/tag'; /** * A book chapter inside the toc list. @@ -52,7 +53,13 @@ export interface AddonModBookTocChapter { * Map of book contents. For each chapter it has its index URL and the list of paths of the files the chapter has. Each path * is identified by the relative path in the book, and the value is the URL of the file. */ -export type AddonModBookContentsMap = {[chapter: string]: {indexUrl?: string, paths: {[path: string]: string}}}; +export type AddonModBookContentsMap = { + [chapter: string]: { + indexUrl?: string, + paths: {[path: string]: string}, + tags?: CoreTagItem[] + } +}; /** * Service that provides some features for books. @@ -203,8 +210,9 @@ export class AddonModBookProvider { map[chapter] = map[chapter] || { paths: {} }; if (content.filename == 'index.html' && filepathIsChapter) { - // Index of the chapter, set indexUrl of the chapter. + // Index of the chapter, set indexUrl and tags of the chapter. map[chapter].indexUrl = content.fileurl; + map[chapter].tags = content.tags; } else { if (filepathIsChapter) { // It's a file in the root folder OR the WS isn't returning the filepath as it should (MDL-53671). From c07eb58568508fe9f5f27b6392321ed4e9207a92 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 11:19:05 +0200 Subject: [PATCH 086/241] MOBILE-2201 data: Display tags in database entries --- src/addon/mod/data/components/action/action.ts | 6 +++++- .../mod/data/components/action/addon-mod-data-action.html | 2 ++ src/addon/mod/data/components/components.module.ts | 4 +++- src/addon/mod/data/providers/helper.ts | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/addon/mod/data/components/action/action.ts b/src/addon/mod/data/components/action/action.ts index 9e96c3d1aaf..b96dc078dc6 100644 --- a/src/addon/mod/data/components/action/action.ts +++ b/src/addon/mod/data/components/action/action.ts @@ -20,6 +20,7 @@ import { AddonModDataOfflineProvider } from '../../providers/offline'; import { CoreSitesProvider } from '@providers/sites'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreUserProvider } from '@core/user/providers/user'; +import { CoreTagProvider } from '@core/tag/providers/tag'; /** * Component that displays a database action. @@ -41,13 +42,16 @@ export class AddonModDataActionComponent implements OnInit { rootUrl: string; url: string; userPicture: string; + tagsEnabled: boolean; constructor(protected injector: Injector, protected dataProvider: AddonModDataProvider, protected dataOffline: AddonModDataOfflineProvider, protected eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, protected userProvider: CoreUserProvider, private navCtrl: NavController, - protected linkHelper: CoreContentLinksHelperProvider, private dataHelper: AddonModDataHelperProvider) { + protected linkHelper: CoreContentLinksHelperProvider, private dataHelper: AddonModDataHelperProvider, + private tagProvider: CoreTagProvider) { this.rootUrl = sitesProvider.getCurrentSite().getURL(); this.siteId = sitesProvider.getCurrentSiteId(); + this.tagsEnabled = this.tagProvider.areTagsAvailableInSite(); } /** diff --git a/src/addon/mod/data/components/action/addon-mod-data-action.html b/src/addon/mod/data/components/action/addon-mod-data-action.html index 41a44e5fafc..b6c9e9924f3 100644 --- a/src/addon/mod/data/components/action/addon-mod-data-action.html +++ b/src/addon/mod/data/components/action/addon-mod-data-action.html @@ -32,3 +32,5 @@ {{entry.fullname}} + + diff --git a/src/addon/mod/data/components/components.module.ts b/src/addon/mod/data/components/components.module.ts index 3470ae8724e..ef12a46b350 100644 --- a/src/addon/mod/data/components/components.module.ts +++ b/src/addon/mod/data/components/components.module.ts @@ -25,6 +25,7 @@ import { AddonModDataFieldPluginComponent } from './field-plugin/field-plugin'; import { AddonModDataActionComponent } from './action/action'; import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module'; import { CoreCommentsComponentsModule } from '@core/comments/components/components.module'; +import { CoreTagComponentsModule } from '@core/tag/components/components.module'; @NgModule({ declarations: [ @@ -41,7 +42,8 @@ import { CoreCommentsComponentsModule } from '@core/comments/components/componen CorePipesModule, CoreCourseComponentsModule, CoreCompileHtmlComponentModule, - CoreCommentsComponentsModule + CoreCommentsComponentsModule, + CoreTagComponentsModule ], providers: [ ], diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts index b5aaadcec53..477eed3fc82 100644 --- a/src/addon/mod/data/providers/helper.ts +++ b/src/addon/mod/data/providers/helper.ts @@ -367,6 +367,7 @@ export class AddonModDataHelperProvider { userpicture: true, timeadded: true, timemodified: true, + tags: true, edit: record.canmanageentry && !record.deleted, // This already checks capabilities and readonly period. delete: record.canmanageentry, @@ -377,7 +378,6 @@ export class AddonModDataHelperProvider { comments: database.comments, // Unsupported actions. - tags: false, delcheck: false, export: false }; From c00cbb9b8ddb8734c72e533f21a7bd6590ad547b Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 11:21:48 +0200 Subject: [PATCH 087/241] MOBILE-2201 data: Display message in pages where tags are not supported --- scripts/langindex.json | 2 ++ src/addon/mod/data/lang/en.json | 2 ++ src/addon/mod/data/pages/edit/edit.ts | 9 ++++++++- src/addon/mod/data/pages/search/search.ts | 9 ++++++--- src/assets/lang/en.json | 2 ++ 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 89bf47b41f1..d9741522caa 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -431,6 +431,7 @@ "addon.mod_data.confirmdeleterecord": "data", "addon.mod_data.descending": "data", "addon.mod_data.disapprove": "data", + "addon.mod_data.edittagsnotsupported": "local_moodlemobileapp", "addon.mod_data.emptyaddform": "data", "addon.mod_data.entrieslefttoadd": "data", "addon.mod_data.entrieslefttoaddtoview": "data", @@ -455,6 +456,7 @@ "addon.mod_data.recorddisapproved": "data", "addon.mod_data.resetsettings": "data", "addon.mod_data.search": "data", + "addon.mod_data.searchbytagsnotsupported": "local_moodlemobileapp", "addon.mod_data.selectedrequired": "data", "addon.mod_data.single": "data", "addon.mod_data.timeadded": "data", diff --git a/src/addon/mod/data/lang/en.json b/src/addon/mod/data/lang/en.json index f358c48a870..f7c0005dc5f 100644 --- a/src/addon/mod/data/lang/en.json +++ b/src/addon/mod/data/lang/en.json @@ -10,6 +10,7 @@ "confirmdeleterecord": "Are you sure you want to delete this entry?", "descending": "Descending", "disapprove": "Undo approval", + "edittagsnotsupported": "Sorry, editing tags is not supported by the app.", "emptyaddform": "You did not fill out any fields!", "entrieslefttoadd": "You must add {{$a.entriesleft}} more entry/entries in order to complete this activity", "entrieslefttoaddtoview": "You must add {{$a.entrieslefttoview}} more entry/entries before you can view other participants' entries.", @@ -34,6 +35,7 @@ "recorddisapproved": "Entry unapproved", "resetsettings": "Reset filters", "search": "Search", + "searchbytagsnotsupported": "Sorry, searching by tags is not supported by the app.", "selectedrequired": "All selected required", "single": "View single", "timeadded": "Time added", diff --git a/src/addon/mod/data/pages/edit/edit.ts b/src/addon/mod/data/pages/edit/edit.ts index 35e74cd068f..68d6cdada81 100644 --- a/src/addon/mod/data/pages/edit/edit.ts +++ b/src/addon/mod/data/pages/edit/edit.ts @@ -28,6 +28,7 @@ import { AddonModDataHelperProvider } from '../../providers/helper'; import { AddonModDataOfflineProvider } from '../../providers/offline'; import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; import { AddonModDataComponentsModule } from '../../components/components.module'; +import { CoreTagProvider } from '@core/tag/providers/tag'; /** * Page that displays the view edit page. @@ -68,7 +69,8 @@ export class AddonModDataEditPage { protected courseProvider: CoreCourseProvider, protected dataProvider: AddonModDataProvider, protected dataOffline: AddonModDataOfflineProvider, protected dataHelper: AddonModDataHelperProvider, sitesProvider: CoreSitesProvider, protected navCtrl: NavController, protected translate: TranslateService, - protected eventsProvider: CoreEventsProvider, protected fileUploaderProvider: CoreFileUploaderProvider) { + protected eventsProvider: CoreEventsProvider, protected fileUploaderProvider: CoreFileUploaderProvider, + private tagProvider: CoreTagProvider) { this.module = params.get('module') || {}; this.entryId = params.get('entryId') || null; this.courseId = params.get('courseId'); @@ -309,6 +311,11 @@ export class AddonModDataEditPage { template = template.replace(replace, 'field_' + field.id); }); + // Editing tags is not supported. + replace = new RegExp('##tags##', 'gi'); + const message = '

{{ \'addon.mod_data.edittagsnotsupported\' | translate }}

'; + template = template.replace(replace, this.tagProvider.areTagsAvailableInSite() ? message : ''); + return template; } diff --git a/src/addon/mod/data/pages/search/search.ts b/src/addon/mod/data/pages/search/search.ts index 4ca20b5d04e..fb0a1649595 100644 --- a/src/addon/mod/data/pages/search/search.ts +++ b/src/addon/mod/data/pages/search/search.ts @@ -21,6 +21,7 @@ import { CoreTextUtilsProvider } from '@providers/utils/text'; import { AddonModDataComponentsModule } from '../../components/components.module'; import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; import { AddonModDataHelperProvider } from '../../providers/helper'; +import { CoreTagProvider } from '@core/tag/providers/tag'; /** * Page that displays the search modal. @@ -42,7 +43,8 @@ export class AddonModDataSearchPage { constructor(params: NavParams, private viewCtrl: ViewController, fb: FormBuilder, protected utils: CoreUtilsProvider, protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate, - protected textUtils: CoreTextUtilsProvider, protected dataHelper: AddonModDataHelperProvider) { + protected textUtils: CoreTextUtilsProvider, protected dataHelper: AddonModDataHelperProvider, + private tagProvider: CoreTagProvider) { this.search = params.get('search'); this.fields = params.get('fields'); this.data = params.get('data'); @@ -117,9 +119,10 @@ export class AddonModDataSearchPage { [placeholder]="\'addon.mod_data.authorlastname\' | translate" formControlName="lastname">'; template = template.replace(replace, render); - // Tags are unsupported right now. + // Searching by tags is not supported. replace = new RegExp('##tags##', 'gi'); - template = template.replace(replace, ''); + const message = '

{{ \'addon.mod_data.searchbytagsnotsupported\' | translate }}

'; + template = template.replace(replace, this.tagProvider.areTagsAvailableInSite() ? message : ''); return template; } diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 0dc92ff1336..b75231f8032 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -431,6 +431,7 @@ "addon.mod_data.confirmdeleterecord": "Are you sure you want to delete this entry?", "addon.mod_data.descending": "Descending", "addon.mod_data.disapprove": "Undo approval", + "addon.mod_data.edittagsnotsupported": "Sorry, editing tags is not supported by the app.", "addon.mod_data.emptyaddform": "You did not fill out any fields!", "addon.mod_data.entrieslefttoadd": "You must add {{$a.entriesleft}} more entry/entries in order to complete this activity", "addon.mod_data.entrieslefttoaddtoview": "You must add {{$a.entrieslefttoview}} more entry/entries before you can view other participants' entries.", @@ -455,6 +456,7 @@ "addon.mod_data.recorddisapproved": "Entry unapproved", "addon.mod_data.resetsettings": "Reset filters", "addon.mod_data.search": "Search", + "addon.mod_data.searchbytagsnotsupported": "Sorry, searching by tags is not supported by the app.", "addon.mod_data.selectedrequired": "All selected required", "addon.mod_data.single": "View single", "addon.mod_data.timeadded": "Time added", From 1226a17d81491c29d87bd1856b4a1aa2e6924389 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 11:24:17 +0200 Subject: [PATCH 088/241] MOBILE-2201 forum: Display tags in forum posts --- src/addon/mod/forum/components/components.module.ts | 4 +++- src/addon/mod/forum/components/post/addon-mod-forum-post.html | 4 ++++ src/addon/mod/forum/components/post/post.ts | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/addon/mod/forum/components/components.module.ts b/src/addon/mod/forum/components/components.module.ts index 0f3bf1b10d5..06cdabd01e2 100644 --- a/src/addon/mod/forum/components/components.module.ts +++ b/src/addon/mod/forum/components/components.module.ts @@ -21,6 +21,7 @@ import { CoreDirectivesModule } from '@directives/directives.module'; import { CorePipesModule } from '@pipes/pipes.module'; import { CoreCourseComponentsModule } from '@core/course/components/components.module'; import { CoreRatingComponentsModule } from '@core/rating/components/components.module'; +import { CoreTagComponentsModule } from '@core/tag/components/components.module'; import { AddonModForumIndexComponent } from './index/index'; import { AddonModForumPostComponent } from './post/post'; @@ -37,7 +38,8 @@ import { AddonModForumPostComponent } from './post/post'; CoreDirectivesModule, CorePipesModule, CoreCourseComponentsModule, - CoreRatingComponentsModule + CoreRatingComponentsModule, + CoreTagComponentsModule ], providers: [ ], diff --git a/src/addon/mod/forum/components/post/addon-mod-forum-post.html b/src/addon/mod/forum/components/post/addon-mod-forum-post.html index 9119c2beda8..81107408cd5 100644 --- a/src/addon/mod/forum/components/post/addon-mod-forum-post.html +++ b/src/addon/mod/forum/components/post/addon-mod-forum-post.html @@ -30,6 +30,10 @@

+ +
{{ 'core.tag.tags' | translate }}:
+ +
diff --git a/src/addon/mod/forum/components/post/post.ts b/src/addon/mod/forum/components/post/post.ts index a8d010a9eb0..d6a9ca54d1d 100644 --- a/src/addon/mod/forum/components/post/post.ts +++ b/src/addon/mod/forum/components/post/post.ts @@ -25,6 +25,7 @@ import { AddonModForumHelperProvider } from '../../providers/helper'; import { AddonModForumOfflineProvider } from '../../providers/offline'; import { AddonModForumSyncProvider } from '../../providers/sync'; import { CoreRatingInfo } from '@core/rating/providers/rating'; +import { CoreTagProvider } from '@core/tag/providers/tag'; /** * Components that shows a discussion post, its attachments and the action buttons allowed (reply, etc.). @@ -52,6 +53,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy { uniqueId: string; advanced = false; // Display all form fields. + tagsEnabled: boolean; protected syncId: string; @@ -65,8 +67,10 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy { private forumHelper: AddonModForumHelperProvider, private forumOffline: AddonModForumOfflineProvider, private forumSync: AddonModForumSyncProvider, + private tagProvider: CoreTagProvider, @Optional() private content: Content) { this.onPostChange = new EventEmitter(); + this.tagsEnabled = this.tagProvider.areTagsAvailableInSite(); } /** From 4400d99638449f0e5480f3740c45950d3ed3c35f Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 11:28:04 +0200 Subject: [PATCH 089/241] MOBILE-2201 glossary: Display tags in glossary entries --- src/addon/mod/glossary/pages/entry/entry.html | 4 ++++ src/addon/mod/glossary/pages/entry/entry.module.ts | 4 +++- src/addon/mod/glossary/pages/entry/entry.ts | 6 +++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/addon/mod/glossary/pages/entry/entry.html b/src/addon/mod/glossary/pages/entry/entry.html index f34e46d2777..5954398ff4e 100644 --- a/src/addon/mod/glossary/pages/entry/entry.html +++ b/src/addon/mod/glossary/pages/entry/entry.html @@ -28,6 +28,10 @@

+ +
{{ 'core.tag.tags' | translate }}:
+ +

{{ 'addon.mod_glossary.entrypendingapproval' | translate }}

diff --git a/src/addon/mod/glossary/pages/entry/entry.module.ts b/src/addon/mod/glossary/pages/entry/entry.module.ts index cc69e9dc4cf..7309430914e 100644 --- a/src/addon/mod/glossary/pages/entry/entry.module.ts +++ b/src/addon/mod/glossary/pages/entry/entry.module.ts @@ -19,6 +19,7 @@ import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; import { CorePipesModule } from '@pipes/pipes.module'; import { CoreRatingComponentsModule } from '@core/rating/components/components.module'; +import { CoreTagComponentsModule } from '@core/tag/components/components.module'; import { AddonModGlossaryEntryPage } from './entry'; @NgModule({ @@ -31,7 +32,8 @@ import { AddonModGlossaryEntryPage } from './entry'; CorePipesModule, IonicPageModule.forChild(AddonModGlossaryEntryPage), TranslateModule.forChild(), - CoreRatingComponentsModule + CoreRatingComponentsModule, + CoreTagComponentsModule ], }) export class AddonModForumDiscussionPageModule {} diff --git a/src/addon/mod/glossary/pages/entry/entry.ts b/src/addon/mod/glossary/pages/entry/entry.ts index 7bbf4f0bf19..a5b38641d0c 100644 --- a/src/addon/mod/glossary/pages/entry/entry.ts +++ b/src/addon/mod/glossary/pages/entry/entry.ts @@ -16,6 +16,7 @@ import { Component } from '@angular/core'; import { IonicPage, NavParams } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreRatingInfo } from '@core/rating/providers/rating'; +import { CoreTagProvider } from '@core/tag/providers/tag'; import { AddonModGlossaryProvider } from '../../providers/glossary'; /** @@ -35,15 +36,18 @@ export class AddonModGlossaryEntryPage { showAuthor = false; showDate = false; ratingInfo: CoreRatingInfo; + tagsEnabled: boolean; protected courseId: number; protected entryId: number; constructor(navParams: NavParams, private domUtils: CoreDomUtilsProvider, - private glossaryProvider: AddonModGlossaryProvider) { + private glossaryProvider: AddonModGlossaryProvider, + private tagProvider: CoreTagProvider) { this.courseId = navParams.get('courseId'); this.entryId = navParams.get('entryId'); + this.tagsEnabled = this.tagProvider.areTagsAvailableInSite(); } /** From 75929f6b4f8fb797774b2043f50bc47145cc5367 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 11:29:04 +0200 Subject: [PATCH 090/241] MOBILE-2201 wiki: Display tags in wiki pages --- src/addon/mod/wiki/components/components.module.ts | 4 +++- .../mod/wiki/components/index/addon-mod-wiki-index.html | 5 +++++ src/addon/mod/wiki/components/index/index.ts | 6 +++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/addon/mod/wiki/components/components.module.ts b/src/addon/mod/wiki/components/components.module.ts index 39372cfe232..638be75aa64 100644 --- a/src/addon/mod/wiki/components/components.module.ts +++ b/src/addon/mod/wiki/components/components.module.ts @@ -19,6 +19,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreCourseComponentsModule } from '@core/course/components/components.module'; +import { CoreTagComponentsModule } from '@core/tag/components/components.module'; import { AddonModWikiIndexComponent } from './index/index'; import { AddonModWikiSubwikiPickerComponent } from './subwiki-picker/subwiki-picker'; @@ -33,7 +34,8 @@ import { AddonModWikiSubwikiPickerComponent } from './subwiki-picker/subwiki-pic TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, - CoreCourseComponentsModule + CoreCourseComponentsModule, + CoreTagComponentsModule ], providers: [ ], diff --git a/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html b/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html index 3d883459522..2ee9b5e8dd6 100644 --- a/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html +++ b/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html @@ -50,6 +50,11 @@ + +
+ {{ 'core.tag.tags' | translate }}: + +
diff --git a/src/addon/mod/wiki/components/index/index.ts b/src/addon/mod/wiki/components/index/index.ts index 941417a6868..7b4a69f1621 100644 --- a/src/addon/mod/wiki/components/index/index.ts +++ b/src/addon/mod/wiki/components/index/index.ts @@ -23,6 +23,7 @@ import { AddonModWikiOfflineProvider } from '../../providers/wiki-offline'; import { AddonModWikiSyncProvider } from '../../providers/wiki-sync'; import { CoreTabsComponent } from '@components/tabs/tabs'; import { AddonModWikiSubwikiPickerComponent } from '../../components/subwiki-picker/subwiki-picker'; +import { CoreTagProvider } from '@core/tag/providers/tag'; /** * Component that displays a wiki entry page. @@ -64,6 +65,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp subwikis: [], count: 0 }; + tagsEnabled: boolean; protected syncEventName = AddonModWikiSyncProvider.AUTO_SYNCED; protected currentSubwiki: any; // Current selected subwiki. @@ -81,10 +83,12 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp constructor(injector: Injector, protected wikiProvider: AddonModWikiProvider, @Optional() protected content: Content, protected wikiOffline: AddonModWikiOfflineProvider, protected wikiSync: AddonModWikiSyncProvider, protected navCtrl: NavController, protected utils: CoreUtilsProvider, protected groupsProvider: CoreGroupsProvider, - protected userProvider: CoreUserProvider, private popoverCtrl: PopoverController) { + protected userProvider: CoreUserProvider, private popoverCtrl: PopoverController, + private tagProvider: CoreTagProvider) { super(injector, content); this.pageStr = this.translate.instant('addon.mod_wiki.wikipage'); + this.tagsEnabled = this.tagProvider.areTagsAvailableInSite(); } /** From 7546ac9e28f200db80fdf9ea532332065957a024 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 11:48:52 +0200 Subject: [PATCH 091/241] MOBILE-2201 tag: Area delegate and helpers for handlers --- src/core/tag/components/components.module.ts | 4 + .../tag/components/feed/core-tag-feed.html | 8 ++ src/core/tag/components/feed/feed.ts | 26 +++++ src/core/tag/providers/area-delegate.ts | 98 +++++++++++++++++++ src/core/tag/providers/helper.ts | 81 +++++++++++++++ src/core/tag/tag.module.ts | 4 + 6 files changed, 221 insertions(+) create mode 100644 src/core/tag/components/feed/core-tag-feed.html create mode 100644 src/core/tag/components/feed/feed.ts create mode 100644 src/core/tag/providers/area-delegate.ts create mode 100644 src/core/tag/providers/helper.ts diff --git a/src/core/tag/components/components.module.ts b/src/core/tag/components/components.module.ts index c2e07f85bfa..8960002cdc0 100644 --- a/src/core/tag/components/components.module.ts +++ b/src/core/tag/components/components.module.ts @@ -16,11 +16,13 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; +import { CoreTagFeedComponent } from './feed/feed'; import { CoreTagListComponent } from './list/list'; import { CoreDirectivesModule } from '@directives/directives.module'; @NgModule({ declarations: [ + CoreTagFeedComponent, CoreTagListComponent ], imports: [ @@ -32,9 +34,11 @@ import { CoreDirectivesModule } from '@directives/directives.module'; providers: [ ], exports: [ + CoreTagFeedComponent, CoreTagListComponent ], entryComponents: [ + CoreTagFeedComponent ] }) export class CoreTagComponentsModule {} diff --git a/src/core/tag/components/feed/core-tag-feed.html b/src/core/tag/components/feed/core-tag-feed.html new file mode 100644 index 00000000000..fe4a02e21a2 --- /dev/null +++ b/src/core/tag/components/feed/core-tag-feed.html @@ -0,0 +1,8 @@ + + + + + +

{{ item.heading }}

+

{{ text }}

+
diff --git a/src/core/tag/components/feed/feed.ts b/src/core/tag/components/feed/feed.ts new file mode 100644 index 00000000000..5c554f7c39d --- /dev/null +++ b/src/core/tag/components/feed/feed.ts @@ -0,0 +1,26 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Input } from '@angular/core'; + +/** + * Component to render a tag area that uses the "core_tag/tagfeed" web template. + */ +@Component({ + selector: 'core-tag-feed', + templateUrl: 'core-tag-feed.html' +}) +export class CoreTagFeedComponent { + @Input() items: any[]; // Area items to render. +} diff --git a/src/core/tag/providers/area-delegate.ts b/src/core/tag/providers/area-delegate.ts new file mode 100644 index 00000000000..2b0e2ffb861 --- /dev/null +++ b/src/core/tag/providers/area-delegate.ts @@ -0,0 +1,98 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; + +/** + * Interface that all tag area handlers must implement. + */ +export interface CoreTagAreaHandler extends CoreDelegateHandler { + /** + * Component and item type separated by a slash. E.g. 'core/course_modules'. + * @type {string} + */ + type: string; + + /** + * Parses the rendered content of a tag index and returns the items. + * + * @param {string} content Rendered content. + * @return {any[]|Promise} Area items (or promise resolved with the items). + */ + parseContent(content: string): any[] | Promise; + + /** + * Get the component to use to display items. + * + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector): any | Promise; +} + +/** + * Delegate to register tag area handlers. + */ +@Injectable() +export class CoreTagAreaDelegate extends CoreDelegate { + + protected handlerNameProperty = 'type'; + + constructor(logger: CoreLoggerProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider) { + super('CoreTagAreaDelegate', logger, sitesProvider, eventsProvider); + } + + /** + * Returns the display name string for this area. + * + * @param {string} component Component name. + * @param {string} itemType Item type. + * @return {string} String key. + */ + getDisplayNameKey(component: string, itemType: string): string { + return (component == 'core' ? 'core.tag' : 'addon.' + component) + '.tagarea_' + itemType; + } + + /** + * Parses the rendered content of a tag index and returns the items. + * + * @param {string} component Component name. + * @param {string} itemType Item type. + * @param {string} content Rendered content. + * @return {Promise} Promise resolved with the area items, or undefined if not found. + */ + parseContent(component: string, itemType: string, content: string): Promise { + const type = component + '/' + itemType; + + return Promise.resolve(this.executeFunctionOnEnabled(type, 'parseContent', [content])); + } + + /** + * Get the component to use to display an area item. + * + * @param {string} component Component name. + * @param {string} itemType Item type. + * @param {Injector} injector Injector. + * @return {Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(component: string, itemType: string, injector: Injector): Promise { + const type = component + '/' + itemType; + + return Promise.resolve(this.executeFunctionOnEnabled(type, 'getComponent', [injector])); + } +} diff --git a/src/core/tag/providers/helper.ts b/src/core/tag/providers/helper.ts new file mode 100644 index 00000000000..38c097b79cf --- /dev/null +++ b/src/core/tag/providers/helper.ts @@ -0,0 +1,81 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreDomUtilsProvider } from '@providers/utils/dom'; + +/** + * Service with helper functions for tags. + */ +@Injectable() +export class CoreTagHelperProvider { + + constructor(protected domUtils: CoreDomUtilsProvider) {} + + /** + * Parses the rendered content of the "core_tag/tagfeed" web template and returns the items. + * + * @param {string} content Rendered content. + * @return {any[]} Area items. + */ + parseFeedContent(content: string): any[] { + const items = []; + const element = this.domUtils.convertToElement(content); + + Array.from(element.querySelectorAll('ul.tag_feed > li.media')).forEach((itemElement) => { + const item: any = { details: [] }; + + Array.from(itemElement.querySelectorAll('div.media-body > div')).forEach((div: HTMLElement) => { + if (div.classList.contains('media-heading')) { + item.heading = div.innerText.trim(); + const link = div.querySelector('a'); + if (link) { + item.url = link.getAttribute('href'); + } + } else { + // Separate details by lines. + const lines = ['']; + Array.from(div.childNodes).forEach((childNode: Node) => { + if (childNode.nodeType == Node.TEXT_NODE) { + lines[lines.length - 1] += childNode.textContent; + } else if (childNode.nodeType == Node.ELEMENT_NODE) { + const childElement = childNode as HTMLElement; + if (childElement.tagName == 'BR') { + lines.push(''); + } else { + lines[lines.length - 1] += childElement.innerText; + } + } + }); + item.details.push(...lines.map((line) => line.trim()).filter((line) => line != '')); + } + }); + + const image = itemElement.querySelector('div.itemimage img'); + if (image) { + if (image.classList.contains('userpicture')) { + item.avatarUrl = image.getAttribute('src'); + } else { + item.iconUrl = image.getAttribute('src'); + } + } + + if (item.heading && item.url) { + items.push(item); + } + }); + + return items; + } +} diff --git a/src/core/tag/tag.module.ts b/src/core/tag/tag.module.ts index eaa2f9e81b8..45d4d69af97 100644 --- a/src/core/tag/tag.module.ts +++ b/src/core/tag/tag.module.ts @@ -14,6 +14,8 @@ import { NgModule } from '@angular/core'; import { CoreTagProvider } from './providers/tag'; +import { CoreTagHelperProvider } from './providers/helper'; +import { CoreTagAreaDelegate } from './providers/area-delegate'; @NgModule({ declarations: [ @@ -22,6 +24,8 @@ import { CoreTagProvider } from './providers/tag'; ], providers: [ CoreTagProvider, + CoreTagHelperProvider, + CoreTagAreaDelegate ] }) export class CoreTagModule { From eed78c8b6f56a85750149ec1186470c1e3153de2 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 12:02:33 +0200 Subject: [PATCH 092/241] MOBILE-2201 course: Tag area handler for courses --- scripts/langindex.json | 1 + src/assets/lang/en.json | 1 + .../course/components/components.module.ts | 6 +- .../tag-area/core-course-tag-area.html | 5 ++ .../course/components/tag-area/tag-area.ts | 43 +++++++++++ src/core/course/course.module.ts | 9 ++- .../providers/course-tag-area-handler.ts | 74 +++++++++++++++++++ src/core/tag/lang/en.json | 1 + 8 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 src/core/course/components/tag-area/core-course-tag-area.html create mode 100644 src/core/course/components/tag-area/tag-area.ts create mode 100644 src/core/course/providers/course-tag-area-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index d9741522caa..b9db7a280f0 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1795,6 +1795,7 @@ "core.submit": "moodle", "core.success": "moodle", "core.tablet": "local_moodlemobileapp", + "core.tag.tagarea_course": "moodle", "core.tag.tags": "moodle", "core.teachers": "moodle", "core.thereisdatatosync": "local_moodlemobileapp", diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index b75231f8032..1174b5e6ad0 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1795,6 +1795,7 @@ "core.submit": "Submit", "core.success": "Success", "core.tablet": "Tablet", + "core.tag.tagarea_course": "Courses", "core.tag.tags": "Tags", "core.teachers": "Teachers", "core.thereisdatatosync": "There are offline {{$a}} to be synchronised.", diff --git a/src/core/course/components/components.module.ts b/src/core/course/components/components.module.ts index a56f920d859..4b55708e2fc 100644 --- a/src/core/course/components/components.module.ts +++ b/src/core/course/components/components.module.ts @@ -22,6 +22,7 @@ import { CoreCourseFormatComponent } from './format/format'; import { CoreCourseModuleComponent } from './module/module'; import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion'; import { CoreCourseModuleDescriptionComponent } from './module-description/module-description'; +import { CoreCourseTagAreaComponent } from './tag-area/tag-area'; import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module'; @NgModule({ @@ -30,6 +31,7 @@ import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsup CoreCourseModuleComponent, CoreCourseModuleCompletionComponent, CoreCourseModuleDescriptionComponent, + CoreCourseTagAreaComponent, CoreCourseUnsupportedModuleComponent ], imports: [ @@ -46,10 +48,12 @@ import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsup CoreCourseModuleComponent, CoreCourseModuleCompletionComponent, CoreCourseModuleDescriptionComponent, + CoreCourseTagAreaComponent, CoreCourseUnsupportedModuleComponent ], entryComponents: [ - CoreCourseUnsupportedModuleComponent + CoreCourseUnsupportedModuleComponent, + CoreCourseTagAreaComponent ] }) export class CoreCourseComponentsModule {} diff --git a/src/core/course/components/tag-area/core-course-tag-area.html b/src/core/course/components/tag-area/core-course-tag-area.html new file mode 100644 index 00000000000..b372fdf0915 --- /dev/null +++ b/src/core/course/components/tag-area/core-course-tag-area.html @@ -0,0 +1,5 @@ + + +

{{ item.courseName }}

+

{{ 'core.category' | translate }}: {{ item.categoryName }}

+
diff --git a/src/core/course/components/tag-area/tag-area.ts b/src/core/course/components/tag-area/tag-area.ts new file mode 100644 index 00000000000..07d34c21c19 --- /dev/null +++ b/src/core/course/components/tag-area/tag-area.ts @@ -0,0 +1,43 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Input, Optional } from '@angular/core'; +import { NavController } from 'ionic-angular'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; + +/** + * Component that renders the course tag area. + */ +@Component({ + selector: 'core-course-tag-area', + templateUrl: 'core-course-tag-area.html' +}) +export class CoreCourseTagAreaComponent { + @Input() items: any[]; // Area items to render. + + constructor(private navCtrl: NavController, @Optional() private splitviewCtrl: CoreSplitViewComponent, + private courseHelper: CoreCourseHelperProvider) {} + + /** + * Open a course. + * + * @param {number} courseId The course to open. + */ + openCourse(courseId: number): void { + // If this component is inside a split view, use the master nav to open it. + const navCtrl = this.splitviewCtrl ? this.splitviewCtrl.getMasterNav() : this.navCtrl; + this.courseHelper.getAndOpenCourse(navCtrl, courseId); + } +} diff --git a/src/core/course/course.module.ts b/src/core/course/course.module.ts index 5293eda6333..4e8206abf8c 100644 --- a/src/core/course/course.module.ts +++ b/src/core/course/course.module.ts @@ -33,6 +33,8 @@ import { CoreCourseFormatWeeksModule } from './formats/weeks/weeks.module'; import { CoreCourseSyncProvider } from './providers/sync'; import { CoreCourseSyncCronHandler } from './providers/sync-cron-handler'; import { CoreCourseLogCronHandler } from './providers/log-cron-handler'; +import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate'; +import { CoreCourseTagAreaHandler } from './providers/course-tag-area-handler'; // List of providers (without handlers). export const CORE_COURSE_PROVIDERS: any[] = [ @@ -68,15 +70,18 @@ export const CORE_COURSE_PROVIDERS: any[] = [ CoreCourseFormatDefaultHandler, CoreCourseModuleDefaultHandler, CoreCourseSyncCronHandler, - CoreCourseLogCronHandler + CoreCourseLogCronHandler, + CoreCourseTagAreaHandler ], exports: [] }) export class CoreCourseModule { constructor(cronDelegate: CoreCronDelegate, syncHandler: CoreCourseSyncCronHandler, logHandler: CoreCourseLogCronHandler, - platform: Platform, eventsProvider: CoreEventsProvider) { + platform: Platform, eventsProvider: CoreEventsProvider, tagAreaDelegate: CoreTagAreaDelegate, + courseTagAreaHandler: CoreCourseTagAreaHandler) { cronDelegate.register(syncHandler); cronDelegate.register(logHandler); + tagAreaDelegate.registerHandler(courseTagAreaHandler); platform.resume.subscribe(() => { // Log the app is open to keep user in online status. diff --git a/src/core/course/providers/course-tag-area-handler.ts b/src/core/course/providers/course-tag-area-handler.ts new file mode 100644 index 00000000000..066754de601 --- /dev/null +++ b/src/core/course/providers/course-tag-area-handler.ts @@ -0,0 +1,74 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate'; +import { CoreCourseTagAreaComponent } from '../components/tag-area/tag-area'; + +/** + * Handler to support tags. + */ +@Injectable() +export class CoreCourseTagAreaHandler implements CoreTagAreaHandler { + name = 'CoreCourseTagAreaHandler'; + type = 'core/course'; + + constructor(private domUtils: CoreDomUtilsProvider) {} + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Parses the rendered content of a tag index and returns the items. + * + * @param {string} content Rendered content. + * @return {any[]|Promise} Area items (or promise resolved with the items). + */ + parseContent(content: string): any[] | Promise { + const items = []; + const element = this.domUtils.convertToElement(content); + + Array.from(element.querySelectorAll('div.coursebox')).forEach((coursebox) => { + const courseId = parseInt(coursebox.getAttribute('data-courseid'), 10); + const courseLink = coursebox.querySelector('.coursename > a'); + const categoryLink = coursebox.querySelector('.coursecat > a'); + + if (courseId > 0 && courseLink) { + items.push({ + courseId, + courseName: courseLink.innerHTML, + categoryName: categoryLink ? categoryLink.innerHTML : null + }); + } + }); + + return items; + } + + /** + * Get the component to use to display items. + * + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector): any | Promise { + return CoreCourseTagAreaComponent; + } +} diff --git a/src/core/tag/lang/en.json b/src/core/tag/lang/en.json index fec56bb365c..c8303131cda 100644 --- a/src/core/tag/lang/en.json +++ b/src/core/tag/lang/en.json @@ -1,3 +1,4 @@ { + "tagarea_course": "Courses", "tags": "Tags" } From fdae95d2946f593b120d78f46880d24a2a63655d Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 12:07:18 +0200 Subject: [PATCH 093/241] MOBILE-2201 course: Tag area handler for activities and resources --- scripts/langindex.json | 1 + src/assets/lang/en.json | 1 + src/core/course/course.module.ts | 7 ++- .../providers/modules-tag-area-handler.ts | 57 +++++++++++++++++++ src/core/tag/lang/en.json | 1 + 5 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 src/core/course/providers/modules-tag-area-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index b9db7a280f0..f36b31fdfdc 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1796,6 +1796,7 @@ "core.success": "moodle", "core.tablet": "local_moodlemobileapp", "core.tag.tagarea_course": "moodle", + "core.tag.tagarea_course_modules": "moodle", "core.tag.tags": "moodle", "core.teachers": "moodle", "core.thereisdatatosync": "local_moodlemobileapp", diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 1174b5e6ad0..3dc88e71c5b 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1796,6 +1796,7 @@ "core.success": "Success", "core.tablet": "Tablet", "core.tag.tagarea_course": "Courses", + "core.tag.tagarea_course_modules": "Activities and resources", "core.tag.tags": "Tags", "core.teachers": "Teachers", "core.thereisdatatosync": "There are offline {{$a}} to be synchronised.", diff --git a/src/core/course/course.module.ts b/src/core/course/course.module.ts index 4e8206abf8c..d14844582ab 100644 --- a/src/core/course/course.module.ts +++ b/src/core/course/course.module.ts @@ -35,6 +35,7 @@ import { CoreCourseSyncCronHandler } from './providers/sync-cron-handler'; import { CoreCourseLogCronHandler } from './providers/log-cron-handler'; import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate'; import { CoreCourseTagAreaHandler } from './providers/course-tag-area-handler'; +import { CoreCourseModulesTagAreaHandler } from './providers/modules-tag-area-handler'; // List of providers (without handlers). export const CORE_COURSE_PROVIDERS: any[] = [ @@ -71,17 +72,19 @@ export const CORE_COURSE_PROVIDERS: any[] = [ CoreCourseModuleDefaultHandler, CoreCourseSyncCronHandler, CoreCourseLogCronHandler, - CoreCourseTagAreaHandler + CoreCourseTagAreaHandler, + CoreCourseModulesTagAreaHandler ], exports: [] }) export class CoreCourseModule { constructor(cronDelegate: CoreCronDelegate, syncHandler: CoreCourseSyncCronHandler, logHandler: CoreCourseLogCronHandler, platform: Platform, eventsProvider: CoreEventsProvider, tagAreaDelegate: CoreTagAreaDelegate, - courseTagAreaHandler: CoreCourseTagAreaHandler) { + courseTagAreaHandler: CoreCourseTagAreaHandler, modulesTagAreaHandler: CoreCourseModulesTagAreaHandler) { cronDelegate.register(syncHandler); cronDelegate.register(logHandler); tagAreaDelegate.registerHandler(courseTagAreaHandler); + tagAreaDelegate.registerHandler(modulesTagAreaHandler); platform.resume.subscribe(() => { // Log the app is open to keep user in online status. diff --git a/src/core/course/providers/modules-tag-area-handler.ts b/src/core/course/providers/modules-tag-area-handler.ts new file mode 100644 index 00000000000..4b7dcfc0a7d --- /dev/null +++ b/src/core/course/providers/modules-tag-area-handler.ts @@ -0,0 +1,57 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate'; +import { CoreTagHelperProvider } from '@core/tag/providers/helper'; +import { CoreTagFeedComponent } from '@core/tag/components/feed/feed'; + +/** + * Handler to support tags. + */ +@Injectable() +export class CoreCourseModulesTagAreaHandler implements CoreTagAreaHandler { + name = 'CoreCourseModulesTagAreaHandler'; + type = 'core/course_modules'; + + constructor(protected tagHelper: CoreTagHelperProvider) {} + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Parses the rendered content of a tag index and returns the items. + * + * @param {string} content Rendered content. + * @return {any[]|Promise} Area items (or promise resolved with the items). + */ + parseContent(content: string): any[] | Promise { + return this.tagHelper.parseFeedContent(content); + } + + /** + * Get the component to use to display items. + * + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector): any | Promise { + return CoreTagFeedComponent; + } +} diff --git a/src/core/tag/lang/en.json b/src/core/tag/lang/en.json index c8303131cda..3275d27af3a 100644 --- a/src/core/tag/lang/en.json +++ b/src/core/tag/lang/en.json @@ -1,4 +1,5 @@ { "tagarea_course": "Courses", + "tagarea_course_modules": "Activities and resources", "tags": "Tags" } From df423b640e45a3115debd2e7573a0f291ece839d Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 12:09:57 +0200 Subject: [PATCH 094/241] MOBILE-2201 blog: Tag area handler for blog posts --- scripts/langindex.json | 1 + src/addon/blog/blog.module.ts | 9 ++- src/addon/blog/providers/tag-area-handler.ts | 58 ++++++++++++++++++++ src/assets/lang/en.json | 1 + src/core/tag/lang/en.json | 1 + 5 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/addon/blog/providers/tag-area-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index f36b31fdfdc..c40287ac5cf 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1797,6 +1797,7 @@ "core.tablet": "local_moodlemobileapp", "core.tag.tagarea_course": "moodle", "core.tag.tagarea_course_modules": "moodle", + "core.tag.tagarea_post": "moodle", "core.tag.tags": "moodle", "core.teachers": "moodle", "core.thereisdatatosync": "local_moodlemobileapp", diff --git a/src/addon/blog/blog.module.ts b/src/addon/blog/blog.module.ts index f3137274535..b697ba27a3d 100644 --- a/src/addon/blog/blog.module.ts +++ b/src/addon/blog/blog.module.ts @@ -17,12 +17,14 @@ import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; import { CoreUserDelegate } from '@core/user/providers/user-delegate'; import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; +import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate'; import { AddonBlogProvider } from './providers/blog'; import { AddonBlogMainMenuHandler } from './providers/mainmenu-handler'; import { AddonBlogUserHandler } from './providers/user-handler'; import { AddonBlogCourseOptionHandler } from './providers/course-option-handler'; import { AddonBlogComponentsModule } from './components/components.module'; import { AddonBlogIndexLinkHandler } from './providers/index-link-handler'; +import { AddonBlogTagAreaHandler } from './providers/tag-area-handler'; @NgModule({ declarations: [ @@ -35,17 +37,20 @@ import { AddonBlogIndexLinkHandler } from './providers/index-link-handler'; AddonBlogMainMenuHandler, AddonBlogUserHandler, AddonBlogCourseOptionHandler, - AddonBlogIndexLinkHandler + AddonBlogIndexLinkHandler, + AddonBlogTagAreaHandler ] }) export class AddonBlogModule { constructor(mainMenuDelegate: CoreMainMenuDelegate, menuHandler: AddonBlogMainMenuHandler, userHandler: AddonBlogUserHandler, userDelegate: CoreUserDelegate, courseOptionHandler: AddonBlogCourseOptionHandler, courseOptionsDelegate: CoreCourseOptionsDelegate, - linkHandler: AddonBlogIndexLinkHandler, contentLinksDelegate: CoreContentLinksDelegate) { + linkHandler: AddonBlogIndexLinkHandler, contentLinksDelegate: CoreContentLinksDelegate, + tagAreaDelegate: CoreTagAreaDelegate, tagAreaHandler: AddonBlogTagAreaHandler) { mainMenuDelegate.registerHandler(menuHandler); userDelegate.registerHandler(userHandler); courseOptionsDelegate.registerHandler(courseOptionHandler); contentLinksDelegate.registerHandler(linkHandler); + tagAreaDelegate.registerHandler(tagAreaHandler); } } diff --git a/src/addon/blog/providers/tag-area-handler.ts b/src/addon/blog/providers/tag-area-handler.ts new file mode 100644 index 00000000000..41413d82437 --- /dev/null +++ b/src/addon/blog/providers/tag-area-handler.ts @@ -0,0 +1,58 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate'; +import { CoreTagHelperProvider } from '@core/tag/providers/helper'; +import { CoreTagFeedComponent } from '@core/tag/components/feed/feed'; +import { AddonBlogProvider } from './blog'; + +/** + * Handler to support tags. + */ +@Injectable() +export class AddonBlogTagAreaHandler implements CoreTagAreaHandler { + name = 'AddonBlogTagAreaHandler'; + type = 'core/post'; + + constructor(private tagHelper: CoreTagHelperProvider, private blogProvider: AddonBlogProvider) {} + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return this.blogProvider.isPluginEnabled(); + } + + /** + * Parses the rendered content of a tag index and returns the items. + * + * @param {string} content Rendered content. + * @return {any[]|Promise} Area items (or promise resolved with the items). + */ + parseContent(content: string): any[] | Promise { + return this.tagHelper.parseFeedContent(content); + } + + /** + * Get the component to use to display items. + * + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector): any | Promise { + return CoreTagFeedComponent; + } +} diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 3dc88e71c5b..344ae3a2c4c 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1797,6 +1797,7 @@ "core.tablet": "Tablet", "core.tag.tagarea_course": "Courses", "core.tag.tagarea_course_modules": "Activities and resources", + "core.tag.tagarea_post": "Blog posts", "core.tag.tags": "Tags", "core.teachers": "Teachers", "core.thereisdatatosync": "There are offline {{$a}} to be synchronised.", diff --git a/src/core/tag/lang/en.json b/src/core/tag/lang/en.json index 3275d27af3a..ce6aec9f144 100644 --- a/src/core/tag/lang/en.json +++ b/src/core/tag/lang/en.json @@ -1,5 +1,6 @@ { "tagarea_course": "Courses", "tagarea_course_modules": "Activities and resources", + "tagarea_post": "Blog posts", "tags": "Tags" } From b0b1c2f7c9a3d62d77b2460d15f1d3577ec43c29 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 12:13:59 +0200 Subject: [PATCH 095/241] MOBILE-2201 user: Tag area handler for users --- scripts/langindex.json | 1 + src/assets/lang/en.json | 1 + src/core/tag/lang/en.json | 1 + src/core/user/components/components.module.ts | 10 ++- .../tag-area/core-user-tag-area.html | 4 + src/core/user/components/tag-area/tag-area.ts | 26 ++++++ src/core/user/providers/tag-area-handler.ts | 82 +++++++++++++++++++ src/core/user/user.module.ts | 6 +- 8 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 src/core/user/components/tag-area/core-user-tag-area.html create mode 100644 src/core/user/components/tag-area/tag-area.ts create mode 100644 src/core/user/providers/tag-area-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index c40287ac5cf..a0dbf92e174 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1798,6 +1798,7 @@ "core.tag.tagarea_course": "moodle", "core.tag.tagarea_course_modules": "moodle", "core.tag.tagarea_post": "moodle", + "core.tag.tagarea_user": "moodle", "core.tag.tags": "moodle", "core.teachers": "moodle", "core.thereisdatatosync": "local_moodlemobileapp", diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 344ae3a2c4c..f48252c14f9 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1798,6 +1798,7 @@ "core.tag.tagarea_course": "Courses", "core.tag.tagarea_course_modules": "Activities and resources", "core.tag.tagarea_post": "Blog posts", + "core.tag.tagarea_user": "User interests", "core.tag.tags": "Tags", "core.teachers": "Teachers", "core.thereisdatatosync": "There are offline {{$a}} to be synchronised.", diff --git a/src/core/tag/lang/en.json b/src/core/tag/lang/en.json index ce6aec9f144..02e2888491d 100644 --- a/src/core/tag/lang/en.json +++ b/src/core/tag/lang/en.json @@ -2,5 +2,6 @@ "tagarea_course": "Courses", "tagarea_course_modules": "Activities and resources", "tagarea_post": "Blog posts", + "tagarea_user": "User interests", "tags": "Tags" } diff --git a/src/core/user/components/components.module.ts b/src/core/user/components/components.module.ts index 7e7427c178e..e741ad964ff 100644 --- a/src/core/user/components/components.module.ts +++ b/src/core/user/components/components.module.ts @@ -18,6 +18,7 @@ import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreUserParticipantsComponent } from './participants/participants'; import { CoreUserProfileFieldComponent } from './user-profile-field/user-profile-field'; +import { CoreUserTagAreaComponent } from './tag-area/tag-area'; import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; import { CorePipesModule } from '@pipes/pipes.module'; @@ -25,7 +26,8 @@ import { CorePipesModule } from '@pipes/pipes.module'; @NgModule({ declarations: [ CoreUserParticipantsComponent, - CoreUserProfileFieldComponent + CoreUserProfileFieldComponent, + CoreUserTagAreaComponent ], imports: [ CommonModule, @@ -39,10 +41,12 @@ import { CorePipesModule } from '@pipes/pipes.module'; ], exports: [ CoreUserParticipantsComponent, - CoreUserProfileFieldComponent + CoreUserProfileFieldComponent, + CoreUserTagAreaComponent ], entryComponents: [ - CoreUserParticipantsComponent + CoreUserParticipantsComponent, + CoreUserTagAreaComponent ] }) export class CoreUserComponentsModule {} diff --git a/src/core/user/components/tag-area/core-user-tag-area.html b/src/core/user/components/tag-area/core-user-tag-area.html new file mode 100644 index 00000000000..8ca11b857cb --- /dev/null +++ b/src/core/user/components/tag-area/core-user-tag-area.html @@ -0,0 +1,4 @@ + + +

{{ item.fullname }}

+
diff --git a/src/core/user/components/tag-area/tag-area.ts b/src/core/user/components/tag-area/tag-area.ts new file mode 100644 index 00000000000..8c4f016121c --- /dev/null +++ b/src/core/user/components/tag-area/tag-area.ts @@ -0,0 +1,26 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Input } from '@angular/core'; + +/** + * Component to render the user tag area. + */ +@Component({ + selector: 'core-user-tag-area', + templateUrl: 'core-user-tag-area.html' +}) +export class CoreUserTagAreaComponent { + @Input() items: any[]; // Area items to render. +} diff --git a/src/core/user/providers/tag-area-handler.ts b/src/core/user/providers/tag-area-handler.ts new file mode 100644 index 00000000000..ab2d167cf6f --- /dev/null +++ b/src/core/user/providers/tag-area-handler.ts @@ -0,0 +1,82 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate'; +import { CoreUserTagAreaComponent } from '../components/tag-area/tag-area'; + +/** + * Handler to support tags. + */ +@Injectable() +export class CoreUserTagAreaHandler implements CoreTagAreaHandler { + name = 'CoreUserTagAreaHandler'; + type = 'core/user'; + + constructor(private domUtils: CoreDomUtilsProvider) {} + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Parses the rendered content of a tag index and returns the items. + * + * @param {string} content Rendered content. + * @return {any[]|Promise} Area items (or promise resolved with the items). + */ + parseContent(content: string): any[] | Promise { + const items = []; + const element = this.domUtils.convertToElement(content); + + Array.from(element.querySelectorAll('div.user-box')).forEach((userbox: HTMLElement) => { + const item: any = {}; + + const avatarLink = userbox.querySelector('a:first-child'); + if (!avatarLink) { + return; + } + + const profileUrl = avatarLink.getAttribute('href') || ''; + const match = profileUrl.match(/.*\/user\/(?:profile|view)\.php\?id=(\d+)/); + if (!match) { + return; + } + + item.id = parseInt(match[1], 10); + const avatarImg = avatarLink.querySelector('img.userpicture'); + item.profileimageurl = avatarImg ? avatarImg.getAttribute('src') : ''; + item.fullname = userbox.innerText; + + items.push(item); + }); + + return items; + } + + /** + * Get the component to use to display items. + * + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector): any | Promise { + return CoreUserTagAreaComponent; + } +} diff --git a/src/core/user/user.module.ts b/src/core/user/user.module.ts index 845f2179ea7..0370243a687 100644 --- a/src/core/user/user.module.ts +++ b/src/core/user/user.module.ts @@ -30,6 +30,8 @@ import { CoreCronDelegate } from '@providers/cron'; import { CoreUserOfflineProvider } from './providers/offline'; import { CoreUserSyncProvider } from './providers/sync'; import { CoreUserSyncCronHandler } from './providers/sync-cron-handler'; +import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate'; +import { CoreUserTagAreaHandler } from './providers/tag-area-handler'; // List of providers (without handlers). export const CORE_USER_PROVIDERS: any[] = [ @@ -59,6 +61,7 @@ export const CORE_USER_PROVIDERS: any[] = [ CoreUserParticipantsCourseOptionHandler, CoreUserParticipantsLinkHandler, CoreUserSyncCronHandler, + CoreUserTagAreaHandler ] }) export class CoreUserModule { @@ -67,13 +70,14 @@ export class CoreUserModule { contentLinksDelegate: CoreContentLinksDelegate, userLinkHandler: CoreUserProfileLinkHandler, courseOptionHandler: CoreUserParticipantsCourseOptionHandler, linkHandler: CoreUserParticipantsLinkHandler, courseOptionsDelegate: CoreCourseOptionsDelegate, cronDelegate: CoreCronDelegate, - syncHandler: CoreUserSyncCronHandler) { + syncHandler: CoreUserSyncCronHandler, tagAreaDelegate: CoreTagAreaDelegate, tagAreaHandler: CoreUserTagAreaHandler) { userDelegate.registerHandler(userProfileMailHandler); courseOptionsDelegate.registerHandler(courseOptionHandler); contentLinksDelegate.registerHandler(userLinkHandler); contentLinksDelegate.registerHandler(linkHandler); cronDelegate.register(syncHandler); + tagAreaDelegate.registerHandler(tagAreaHandler); eventsProvider.on(CoreEventsProvider.USER_DELETED, (data) => { // Search for userid in params. From 82611bcf3ad0dba4390af8799bc7950a474c0a6d Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 12:15:29 +0200 Subject: [PATCH 096/241] MOBILE-2201 book: Tag area handler for book chapters --- scripts/langindex.json | 1 + src/addon/mod/book/book.module.ts | 9 ++- src/addon/mod/book/lang/en.json | 1 + .../mod/book/providers/tag-area-handler.ts | 75 +++++++++++++++++++ src/assets/lang/en.json | 1 + 5 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 src/addon/mod/book/providers/tag-area-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index a0dbf92e174..57c4bf18d37 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -372,6 +372,7 @@ "addon.mod_assign_submission_onlinetext.wordlimitexceeded": "assignsubmission_onlinetext", "addon.mod_book.errorchapter": "book", "addon.mod_book.modulenameplural": "book", + "addon.mod_book.tagarea_book_chapters": "book", "addon.mod_book.toc": "book", "addon.mod_chat.beep": "chat", "addon.mod_chat.chatreport": "chat", diff --git a/src/addon/mod/book/book.module.ts b/src/addon/mod/book/book.module.ts index cc06e008df6..ea425d1c8c3 100644 --- a/src/addon/mod/book/book.module.ts +++ b/src/addon/mod/book/book.module.ts @@ -22,6 +22,8 @@ import { AddonModBookPrefetchHandler } from './providers/prefetch-handler'; import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate'; +import { AddonModBookTagAreaHandler } from './providers/tag-area-handler'; // List of providers (without handlers). export const ADDON_MOD_BOOK_PROVIDERS: any[] = [ @@ -39,18 +41,21 @@ export const ADDON_MOD_BOOK_PROVIDERS: any[] = [ AddonModBookModuleHandler, AddonModBookLinkHandler, AddonModBookListLinkHandler, - AddonModBookPrefetchHandler + AddonModBookPrefetchHandler, + AddonModBookTagAreaHandler ] }) export class AddonModBookModule { constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModBookModuleHandler, contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModBookLinkHandler, prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModBookPrefetchHandler, - listLinkHandler: AddonModBookListLinkHandler) { + listLinkHandler: AddonModBookListLinkHandler, tagAreaDelegate: CoreTagAreaDelegate, + tagAreaHandler: AddonModBookTagAreaHandler) { moduleDelegate.registerHandler(moduleHandler); contentLinksDelegate.registerHandler(linkHandler); contentLinksDelegate.registerHandler(listLinkHandler); prefetchDelegate.registerHandler(prefetchHandler); + tagAreaDelegate.registerHandler(tagAreaHandler); } } diff --git a/src/addon/mod/book/lang/en.json b/src/addon/mod/book/lang/en.json index 7d1140fe4dd..4f8f32f54ac 100644 --- a/src/addon/mod/book/lang/en.json +++ b/src/addon/mod/book/lang/en.json @@ -1,5 +1,6 @@ { "errorchapter": "Error reading chapter of book.", "modulenameplural": "Books", + "tagarea_book_chapters": "Book chapters", "toc": "Table of contents" } \ No newline at end of file diff --git a/src/addon/mod/book/providers/tag-area-handler.ts b/src/addon/mod/book/providers/tag-area-handler.ts new file mode 100644 index 00000000000..47c456cb655 --- /dev/null +++ b/src/addon/mod/book/providers/tag-area-handler.ts @@ -0,0 +1,75 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate'; +import { CoreTagHelperProvider } from '@core/tag/providers/helper'; +import { CoreTagFeedComponent } from '@core/tag/components/feed/feed'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; +import { AddonModBookProvider } from './book'; + +/** + * Handler to support tags. + */ +@Injectable() +export class AddonModBookTagAreaHandler implements CoreTagAreaHandler { + name = 'AddonModBookTagAreaHandler'; + type = 'mod_book/book_chapters'; + + constructor(private tagHelper: CoreTagHelperProvider, private bookProvider: AddonModBookProvider, + private courseProvider: CoreCourseProvider, private urlUtils: CoreUrlUtilsProvider) {} + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return this.bookProvider.isPluginEnabled(); + } + + /** + * Parses the rendered content of a tag index and returns the items. + * + * @param {string} content Rendered content. + * @return {any[]|Promise} Area items (or promise resolved with the items). + */ + parseContent(content: string): any[] | Promise { + const items = this.tagHelper.parseFeedContent(content); + + // Find module ids of the returned books, they are needed by the link delegate. + return Promise.all(items.map((item) => { + const params = this.urlUtils.extractUrlParams(item.url); + if (params.b && !params.id) { + const bookId = parseInt(params.b, 10); + + return this.courseProvider.getModuleBasicInfoByInstance(bookId, 'book').then((module) => { + item.url += '&id=' + module.id; + }); + } + })).then(() => { + return items; + }); + } + + /** + * Get the component to use to display items. + * + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector): any | Promise { + return CoreTagFeedComponent; + } +} diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index f48252c14f9..121bd0dba59 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -372,6 +372,7 @@ "addon.mod_assign_submission_onlinetext.wordlimitexceeded": "The word limit for this assignment is {{$a.limit}} words and you are attempting to submit {{$a.count}} words. Please review your submission and try again.", "addon.mod_book.errorchapter": "Error reading chapter of book.", "addon.mod_book.modulenameplural": "Books", + "addon.mod_book.tagarea_book_chapters": "Book chapters", "addon.mod_book.toc": "Table of contents", "addon.mod_chat.beep": "Beep", "addon.mod_chat.chatreport": "Chat sessions", From e698537cf74846b13fba9aabdfbc13ac817e353a Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 12:16:41 +0200 Subject: [PATCH 097/241] MOBILE-2201 data: Tag area handler for database records --- scripts/langindex.json | 1 + src/addon/mod/data/data.module.ts | 9 ++- src/addon/mod/data/lang/en.json | 1 + .../mod/data/providers/tag-area-handler.ts | 58 +++++++++++++++++++ src/assets/lang/en.json | 1 + 5 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/addon/mod/data/providers/tag-area-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 57c4bf18d37..ac7fabe305a 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -460,6 +460,7 @@ "addon.mod_data.searchbytagsnotsupported": "local_moodlemobileapp", "addon.mod_data.selectedrequired": "data", "addon.mod_data.single": "data", + "addon.mod_data.tagarea_data_records": "data", "addon.mod_data.timeadded": "data", "addon.mod_data.timemodified": "data", "addon.mod_data.usedate": "data", diff --git a/src/addon/mod/data/data.module.ts b/src/addon/mod/data/data.module.ts index 9babfa364bf..158865aecd3 100644 --- a/src/addon/mod/data/data.module.ts +++ b/src/addon/mod/data/data.module.ts @@ -33,6 +33,8 @@ import { AddonModDataSyncCronHandler } from './providers/sync-cron-handler'; import { AddonModDataOfflineProvider } from './providers/offline'; import { AddonModDataFieldsDelegate } from './providers/fields-delegate'; import { AddonModDataDefaultFieldHandler } from './providers/default-field-handler'; +import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate'; +import { AddonModDataTagAreaHandler } from './providers/tag-area-handler'; import { AddonModDataFieldModule } from './fields/field.module'; import { CoreUpdateManagerProvider } from '@providers/update-manager'; @@ -67,7 +69,8 @@ export const ADDON_MOD_DATA_PROVIDERS: any[] = [ AddonModDataEditLinkHandler, AddonModDataListLinkHandler, AddonModDataSyncCronHandler, - AddonModDataDefaultFieldHandler + AddonModDataDefaultFieldHandler, + AddonModDataTagAreaHandler ] }) export class AddonModDataModule { @@ -77,7 +80,8 @@ export class AddonModDataModule { cronDelegate: CoreCronDelegate, syncHandler: AddonModDataSyncCronHandler, updateManager: CoreUpdateManagerProvider, approveLinkHandler: AddonModDataApproveLinkHandler, deleteLinkHandler: AddonModDataDeleteLinkHandler, showLinkHandler: AddonModDataShowLinkHandler, editLinkHandler: AddonModDataEditLinkHandler, - listLinkHandler: AddonModDataListLinkHandler) { + listLinkHandler: AddonModDataListLinkHandler, tagAreaDelegate: CoreTagAreaDelegate, + tagAreaHandler: AddonModDataTagAreaHandler) { moduleDelegate.registerHandler(moduleHandler); prefetchDelegate.registerHandler(prefetchHandler); @@ -88,6 +92,7 @@ export class AddonModDataModule { contentLinksDelegate.registerHandler(editLinkHandler); contentLinksDelegate.registerHandler(listLinkHandler); cronDelegate.register(syncHandler); + tagAreaDelegate.registerHandler(tagAreaHandler); // Allow migrating the tables from the old app to the new schema. updateManager.registerSiteTableMigration({ diff --git a/src/addon/mod/data/lang/en.json b/src/addon/mod/data/lang/en.json index f7c0005dc5f..219ec090c49 100644 --- a/src/addon/mod/data/lang/en.json +++ b/src/addon/mod/data/lang/en.json @@ -38,6 +38,7 @@ "searchbytagsnotsupported": "Sorry, searching by tags is not supported by the app.", "selectedrequired": "All selected required", "single": "View single", + "tagarea_data_records": "Data records", "timeadded": "Time added", "timemodified": "Time modified", "usedate": "Include in search." diff --git a/src/addon/mod/data/providers/tag-area-handler.ts b/src/addon/mod/data/providers/tag-area-handler.ts new file mode 100644 index 00000000000..f7fb150984a --- /dev/null +++ b/src/addon/mod/data/providers/tag-area-handler.ts @@ -0,0 +1,58 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate'; +import { CoreTagHelperProvider } from '@core/tag/providers/helper'; +import { CoreTagFeedComponent } from '@core/tag/components/feed/feed'; +import { AddonModDataProvider } from './data'; + +/** + * Handler to support tags. + */ +@Injectable() +export class AddonModDataTagAreaHandler implements CoreTagAreaHandler { + name = 'AddonModDataTagAreaHandler'; + type = 'mod_data/data_records'; + + constructor(private tagHelper: CoreTagHelperProvider, private dataProvider: AddonModDataProvider) {} + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return this.dataProvider.isPluginEnabled(); + } + + /** + * Parses the rendered content of a tag index and returns the items. + * + * @param {string} content Rendered content. + * @return {any[]|Promise} Area items (or promise resolved with the items). + */ + parseContent(content: string): any[] | Promise { + return this.tagHelper.parseFeedContent(content); + } + + /** + * Get the component to use to display items. + * + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector): any | Promise { + return CoreTagFeedComponent; + } +} diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 121bd0dba59..ba1bfb7784f 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -460,6 +460,7 @@ "addon.mod_data.searchbytagsnotsupported": "Sorry, searching by tags is not supported by the app.", "addon.mod_data.selectedrequired": "All selected required", "addon.mod_data.single": "View single", + "addon.mod_data.tagarea_data_records": "Data records", "addon.mod_data.timeadded": "Time added", "addon.mod_data.timemodified": "Time modified", "addon.mod_data.usedate": "Include in search.", From f1591b1113e74ccb415a0a448686715d720c8f4b Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 12:17:47 +0200 Subject: [PATCH 098/241] MOBILE-2201 forum: Tag area handler for forum posts --- scripts/langindex.json | 1 + src/addon/mod/forum/forum.module.ts | 9 ++- src/addon/mod/forum/lang/en.json | 1 + .../mod/forum/providers/tag-area-handler.ts | 57 +++++++++++++++++++ src/assets/lang/en.json | 1 + 5 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/addon/mod/forum/providers/tag-area-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index ac7fabe305a..cb110da9c76 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -553,6 +553,7 @@ "addon.mod_forum.reply": "forum", "addon.mod_forum.replyplaceholder": "forum", "addon.mod_forum.subject": "forum", + "addon.mod_forum.tagarea_forum_posts": "forum", "addon.mod_forum.thisforumhasduedate": "forum", "addon.mod_forum.thisforumisdue": "forum", "addon.mod_forum.unlockdiscussion": "forum", diff --git a/src/addon/mod/forum/forum.module.ts b/src/addon/mod/forum/forum.module.ts index 94fe30257c0..b8463c714ea 100644 --- a/src/addon/mod/forum/forum.module.ts +++ b/src/addon/mod/forum/forum.module.ts @@ -18,6 +18,7 @@ import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; import { CorePushNotificationsDelegate } from '@core/pushnotifications/providers/delegate'; +import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate'; import { AddonModForumProvider } from './providers/forum'; import { AddonModForumOfflineProvider } from './providers/offline'; import { AddonModForumHelperProvider } from './providers/helper'; @@ -30,6 +31,7 @@ import { AddonModForumDiscussionLinkHandler } from './providers/discussion-link- import { AddonModForumListLinkHandler } from './providers/list-link-handler'; import { AddonModForumPostLinkHandler } from './providers/post-link-handler'; import { AddonModForumPushClickHandler } from './providers/push-click-handler'; +import { AddonModForumTagAreaHandler } from './providers/tag-area-handler'; import { AddonModForumComponentsModule } from './components/components.module'; import { CoreUpdateManagerProvider } from '@providers/update-manager'; @@ -59,7 +61,8 @@ export const ADDON_MOD_FORUM_PROVIDERS: any[] = [ AddonModForumListLinkHandler, AddonModForumPostLinkHandler, AddonModForumDiscussionLinkHandler, - AddonModForumPushClickHandler + AddonModForumPushClickHandler, + AddonModForumTagAreaHandler ] }) export class AddonModForumModule { @@ -69,7 +72,8 @@ export class AddonModForumModule { indexHandler: AddonModForumIndexLinkHandler, discussionHandler: AddonModForumDiscussionLinkHandler, updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModForumListLinkHandler, pushNotificationsDelegate: CorePushNotificationsDelegate, pushClickHandler: AddonModForumPushClickHandler, - postLinkHandler: AddonModForumPostLinkHandler) { + postLinkHandler: AddonModForumPostLinkHandler, tagAreaDelegate: CoreTagAreaDelegate, + tagAreaHandler: AddonModForumTagAreaHandler) { moduleDelegate.registerHandler(moduleHandler); prefetchDelegate.registerHandler(prefetchHandler); @@ -79,6 +83,7 @@ export class AddonModForumModule { linksDelegate.registerHandler(listLinkHandler); linksDelegate.registerHandler(postLinkHandler); pushNotificationsDelegate.registerClickHandler(pushClickHandler); + tagAreaDelegate.registerHandler(tagAreaHandler); // Allow migrating the tables from the old app to the new schema. updateManager.registerSiteTablesMigration([ diff --git a/src/addon/mod/forum/lang/en.json b/src/addon/mod/forum/lang/en.json index 93e72d2c2d7..dbfac5fd3be 100644 --- a/src/addon/mod/forum/lang/en.json +++ b/src/addon/mod/forum/lang/en.json @@ -50,6 +50,7 @@ "reply": "Reply", "replyplaceholder": "Write your reply...", "subject": "Subject", + "tagarea_forum_posts": "Forum posts", "thisforumhasduedate": "The due date for posting to this forum is {{$a}}.", "thisforumisdue": "The due date for posting to this forum was {{$a}}.", "unlockdiscussion": "Unlock this discussion", diff --git a/src/addon/mod/forum/providers/tag-area-handler.ts b/src/addon/mod/forum/providers/tag-area-handler.ts new file mode 100644 index 00000000000..02505b31c04 --- /dev/null +++ b/src/addon/mod/forum/providers/tag-area-handler.ts @@ -0,0 +1,57 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate'; +import { CoreTagHelperProvider } from '@core/tag/providers/helper'; +import { CoreTagFeedComponent } from '@core/tag/components/feed/feed'; + +/** + * Handler to support tags. + */ +@Injectable() +export class AddonModForumTagAreaHandler implements CoreTagAreaHandler { + name = 'AddonModForumTagAreaHandler'; + type = 'mod_forum/forum_posts'; + + constructor(private tagHelper: CoreTagHelperProvider) {} + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Parses the rendered content of a tag index and returns the items. + * + * @param {string} content Rendered content. + * @return {any[]|Promise} Area items (or promise resolved with the items). + */ + parseContent(content: string): any[] | Promise { + return this.tagHelper.parseFeedContent(content); + } + + /** + * Get the component to use to display items. + * + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector): any | Promise { + return CoreTagFeedComponent; + } +} diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index ba1bfb7784f..cb31d3779ae 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -553,6 +553,7 @@ "addon.mod_forum.reply": "Reply", "addon.mod_forum.replyplaceholder": "Write your reply...", "addon.mod_forum.subject": "Subject", + "addon.mod_forum.tagarea_forum_posts": "Forum posts", "addon.mod_forum.thisforumhasduedate": "The due date for posting to this forum is {{$a}}.", "addon.mod_forum.thisforumisdue": "The due date for posting to this forum was {{$a}}.", "addon.mod_forum.unlockdiscussion": "Unlock this discussion", From 0a770f8528c5b963594ab44e45df107f44a17389 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 12:18:27 +0200 Subject: [PATCH 099/241] MOBILE-2201 glossary: Tag area handler for glossary entries --- scripts/langindex.json | 1 + src/addon/mod/glossary/glossary.module.ts | 9 ++- src/addon/mod/glossary/lang/en.json | 3 +- .../glossary/providers/tag-area-handler.ts | 57 +++++++++++++++++++ src/assets/lang/en.json | 1 + 5 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 src/addon/mod/glossary/providers/tag-area-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index cb110da9c76..2d1efd4f073 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -588,6 +588,7 @@ "addon.mod_glossary.modulenameplural": "glossary", "addon.mod_glossary.noentriesfound": "local_moodlemobileapp", "addon.mod_glossary.searchquery": "local_moodlemobileapp", + "addon.mod_glossary.tagarea_glossary_entries": "glossary", "addon.mod_imscp.deploymenterror": "imscp", "addon.mod_imscp.modulenameplural": "imscp", "addon.mod_imscp.showmoduledescription": "local_moodlemobileapp", diff --git a/src/addon/mod/glossary/glossary.module.ts b/src/addon/mod/glossary/glossary.module.ts index ac311c14449..e3520fbe72a 100644 --- a/src/addon/mod/glossary/glossary.module.ts +++ b/src/addon/mod/glossary/glossary.module.ts @@ -17,6 +17,7 @@ import { CoreCronDelegate } from '@providers/cron'; import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; +import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate'; import { AddonModGlossaryProvider } from './providers/glossary'; import { AddonModGlossaryOfflineProvider } from './providers/offline'; import { AddonModGlossaryHelperProvider } from './providers/helper'; @@ -28,6 +29,7 @@ import { AddonModGlossaryIndexLinkHandler } from './providers/index-link-handler import { AddonModGlossaryEntryLinkHandler } from './providers/entry-link-handler'; import { AddonModGlossaryListLinkHandler } from './providers/list-link-handler'; import { AddonModGlossaryEditLinkHandler } from './providers/edit-link-handler'; +import { AddonModGlossaryTagAreaHandler } from './providers/tag-area-handler'; import { AddonModGlossaryComponentsModule } from './components/components.module'; import { CoreUpdateManagerProvider } from '@providers/update-manager'; @@ -56,7 +58,8 @@ export const ADDON_MOD_GLOSSARY_PROVIDERS: any[] = [ AddonModGlossaryIndexLinkHandler, AddonModGlossaryEntryLinkHandler, AddonModGlossaryListLinkHandler, - AddonModGlossaryEditLinkHandler + AddonModGlossaryEditLinkHandler, + AddonModGlossaryTagAreaHandler ] }) export class AddonModGlossaryModule { @@ -65,7 +68,8 @@ export class AddonModGlossaryModule { cronDelegate: CoreCronDelegate, syncHandler: AddonModGlossarySyncCronHandler, linksDelegate: CoreContentLinksDelegate, indexHandler: AddonModGlossaryIndexLinkHandler, discussionHandler: AddonModGlossaryEntryLinkHandler, updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModGlossaryListLinkHandler, - editLinkHandler: AddonModGlossaryEditLinkHandler) { + editLinkHandler: AddonModGlossaryEditLinkHandler, tagAreaDelegate: CoreTagAreaDelegate, + tagAreaHandler: AddonModGlossaryTagAreaHandler) { moduleDelegate.registerHandler(moduleHandler); prefetchDelegate.registerHandler(prefetchHandler); @@ -74,6 +78,7 @@ export class AddonModGlossaryModule { linksDelegate.registerHandler(discussionHandler); linksDelegate.registerHandler(listLinkHandler); linksDelegate.registerHandler(editLinkHandler); + tagAreaDelegate.registerHandler(tagAreaHandler); // Allow migrating the tables from the old app to the new schema. updateManager.registerSiteTableMigration({ diff --git a/src/addon/mod/glossary/lang/en.json b/src/addon/mod/glossary/lang/en.json index 18e5ff7bcb9..ba4329f33db 100644 --- a/src/addon/mod/glossary/lang/en.json +++ b/src/addon/mod/glossary/lang/en.json @@ -26,5 +26,6 @@ "linking": "Auto-linking", "modulenameplural": "Glossaries", "noentriesfound": "No entries were found.", - "searchquery": "Search query" + "searchquery": "Search query", + "tagarea_glossary_entries": "Glossary entries" } diff --git a/src/addon/mod/glossary/providers/tag-area-handler.ts b/src/addon/mod/glossary/providers/tag-area-handler.ts new file mode 100644 index 00000000000..4c62f698dcb --- /dev/null +++ b/src/addon/mod/glossary/providers/tag-area-handler.ts @@ -0,0 +1,57 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate'; +import { CoreTagHelperProvider } from '@core/tag/providers/helper'; +import { CoreTagFeedComponent } from '@core/tag/components/feed/feed'; + +/** + * Handler to support tags. + */ +@Injectable() +export class AddonModGlossaryTagAreaHandler implements CoreTagAreaHandler { + name = 'AddonModGlossaryTagAreaHandler'; + type = 'mod_glossary/glossary_entries'; + + constructor(private tagHelper: CoreTagHelperProvider) {} + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Parses the rendered content of a tag index and returns the items. + * + * @param {string} content Rendered content. + * @return {any[]|Promise} Area items (or promise resolved with the items). + */ + parseContent(content: string): any[] | Promise { + return this.tagHelper.parseFeedContent(content); + } + + /** + * Get the component to use to display items. + * + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector): any | Promise { + return CoreTagFeedComponent; + } +} diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index cb31d3779ae..00a01189166 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -588,6 +588,7 @@ "addon.mod_glossary.modulenameplural": "Glossaries", "addon.mod_glossary.noentriesfound": "No entries were found.", "addon.mod_glossary.searchquery": "Search query", + "addon.mod_glossary.tagarea_glossary_entries": "Glossary entries", "addon.mod_imscp.deploymenterror": "Content package error!", "addon.mod_imscp.modulenameplural": "IMS content packages", "addon.mod_imscp.showmoduledescription": "Show description", From 353c6823dbbfa2ced392f8131e99709e7cdcabd4 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 12:19:07 +0200 Subject: [PATCH 100/241] MOBILE-2201 wiki: Tag area handler for wiki pages --- scripts/langindex.json | 1 + src/addon/mod/wiki/lang/en.json | 1 + .../mod/wiki/providers/tag-area-handler.ts | 57 +++++++++++++++++++ src/addon/mod/wiki/wiki.module.ts | 9 ++- src/assets/lang/en.json | 1 + 5 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/addon/mod/wiki/providers/tag-area-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 2d1efd4f073..369488b848a 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -843,6 +843,7 @@ "addon.mod_wiki.pageexists": "wiki", "addon.mod_wiki.pagename": "wiki", "addon.mod_wiki.subwiki": "local_moodlemobileapp", + "addon.mod_wiki.tagarea_wiki_pages": "wiki", "addon.mod_wiki.titleshouldnotbeempty": "local_moodlemobileapp", "addon.mod_wiki.viewpage": "local_moodlemobileapp", "addon.mod_wiki.wikipage": "local_moodlemobileapp", diff --git a/src/addon/mod/wiki/lang/en.json b/src/addon/mod/wiki/lang/en.json index 29ed054ce25..246965b9c61 100644 --- a/src/addon/mod/wiki/lang/en.json +++ b/src/addon/mod/wiki/lang/en.json @@ -14,6 +14,7 @@ "pageexists": "This page already exists.", "pagename": "Page name", "subwiki": "Sub-wiki", + "tagarea_wiki_pages": "Wiki pages", "titleshouldnotbeempty": "The title should not be empty", "viewpage": "View page", "wikipage": "Wiki page", diff --git a/src/addon/mod/wiki/providers/tag-area-handler.ts b/src/addon/mod/wiki/providers/tag-area-handler.ts new file mode 100644 index 00000000000..0f3cab521b8 --- /dev/null +++ b/src/addon/mod/wiki/providers/tag-area-handler.ts @@ -0,0 +1,57 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate'; +import { CoreTagHelperProvider } from '@core/tag/providers/helper'; +import { CoreTagFeedComponent } from '@core/tag/components/feed/feed'; + +/** + * Handler to support tags. + */ +@Injectable() +export class AddonModWikiTagAreaHandler implements CoreTagAreaHandler { + name = 'AddonModWikiTagAreaHandler'; + type = 'mod_wiki/wiki_pages'; + + constructor(private tagHelper: CoreTagHelperProvider) {} + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Parses the rendered content of a tag index and returns the items. + * + * @param {string} content Rendered content. + * @return {any[]|Promise} Area items (or promise resolved with the items). + */ + parseContent(content: string): any[] | Promise { + return this.tagHelper.parseFeedContent(content); + } + + /** + * Get the component to use to display items. + * + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector): any | Promise { + return CoreTagFeedComponent; + } +} diff --git a/src/addon/mod/wiki/wiki.module.ts b/src/addon/mod/wiki/wiki.module.ts index 539b962dd30..6396cac7490 100644 --- a/src/addon/mod/wiki/wiki.module.ts +++ b/src/addon/mod/wiki/wiki.module.ts @@ -17,6 +17,7 @@ import { CoreCronDelegate } from '@providers/cron'; import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; +import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate'; import { AddonModWikiComponentsModule } from './components/components.module'; import { AddonModWikiProvider } from './providers/wiki'; import { AddonModWikiOfflineProvider } from './providers/wiki-offline'; @@ -29,6 +30,7 @@ import { AddonModWikiPageOrMapLinkHandler } from './providers/page-or-map-link-h import { AddonModWikiCreateLinkHandler } from './providers/create-link-handler'; import { AddonModWikiEditLinkHandler } from './providers/edit-link-handler'; import { AddonModWikiListLinkHandler } from './providers/list-link-handler'; +import { AddonModWikiTagAreaHandler } from './providers/tag-area-handler'; import { CoreUpdateManagerProvider } from '@providers/update-manager'; // List of providers (without handlers). @@ -55,7 +57,8 @@ export const ADDON_MOD_WIKI_PROVIDERS: any[] = [ AddonModWikiPageOrMapLinkHandler, AddonModWikiCreateLinkHandler, AddonModWikiEditLinkHandler, - AddonModWikiListLinkHandler + AddonModWikiListLinkHandler, + AddonModWikiTagAreaHandler ] }) export class AddonModWikiModule { @@ -64,7 +67,8 @@ export class AddonModWikiModule { cronDelegate: CoreCronDelegate, syncHandler: AddonModWikiSyncCronHandler, linksDelegate: CoreContentLinksDelegate, indexHandler: AddonModWikiIndexLinkHandler, pageOrMapHandler: AddonModWikiPageOrMapLinkHandler, createHandler: AddonModWikiCreateLinkHandler, editHandler: AddonModWikiEditLinkHandler, - updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModWikiListLinkHandler) { + updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModWikiListLinkHandler, + tagAreaDelegate: CoreTagAreaDelegate, tagAreaHandler: AddonModWikiTagAreaHandler) { moduleDelegate.registerHandler(moduleHandler); prefetchDelegate.registerHandler(prefetchHandler); @@ -74,6 +78,7 @@ export class AddonModWikiModule { linksDelegate.registerHandler(createHandler); linksDelegate.registerHandler(editHandler); linksDelegate.registerHandler(listLinkHandler); + tagAreaDelegate.registerHandler(tagAreaHandler); // Allow migrating the tables from the old app to the new schema. updateManager.registerSiteTableMigration({ diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 00a01189166..6fc2be13d02 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -843,6 +843,7 @@ "addon.mod_wiki.pageexists": "This page already exists.", "addon.mod_wiki.pagename": "Page name", "addon.mod_wiki.subwiki": "Sub-wiki", + "addon.mod_wiki.tagarea_wiki_pages": "Wiki pages", "addon.mod_wiki.titleshouldnotbeempty": "The title should not be empty", "addon.mod_wiki.viewpage": "View page", "addon.mod_wiki.wikipage": "Wiki page", From b2db3774e6d909dcea3e90827a5a2a1145c1c4ff Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 12:32:05 +0200 Subject: [PATCH 101/241] MOBILE-2201 tag: Index page --- scripts/langindex.json | 4 + src/assets/lang/en.json | 4 + src/core/tag/lang/en.json | 6 +- src/core/tag/pages/index-area/index-area.html | 16 ++ .../tag/pages/index-area/index-area.module.ts | 33 ++++ src/core/tag/pages/index-area/index-area.ts | 150 +++++++++++++++++ src/core/tag/pages/index/index.html | 24 +++ src/core/tag/pages/index/index.module.ts | 33 ++++ src/core/tag/pages/index/index.ts | 154 ++++++++++++++++++ src/core/tag/providers/tag.ts | 117 ++++++++++++- 10 files changed, 538 insertions(+), 3 deletions(-) create mode 100644 src/core/tag/pages/index-area/index-area.html create mode 100644 src/core/tag/pages/index-area/index-area.module.ts create mode 100644 src/core/tag/pages/index-area/index-area.ts create mode 100644 src/core/tag/pages/index/index.html create mode 100644 src/core/tag/pages/index/index.module.ts create mode 100644 src/core/tag/pages/index/index.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 369488b848a..679e4d55931 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1800,11 +1800,15 @@ "core.submit": "moodle", "core.success": "moodle", "core.tablet": "local_moodlemobileapp", + "core.tag.errorareanotsupported": "local_moodlemobileapp", + "core.tag.itemstaggedwith": "moodle", + "core.tag.tag": "moodle", "core.tag.tagarea_course": "moodle", "core.tag.tagarea_course_modules": "moodle", "core.tag.tagarea_post": "moodle", "core.tag.tagarea_user": "moodle", "core.tag.tags": "moodle", + "core.tag.warningareasnotsupported": "local_moodlemobileapp", "core.teachers": "moodle", "core.thereisdatatosync": "local_moodlemobileapp", "core.thisdirection": "langconfig", diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 6fc2be13d02..69730eab94f 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1800,11 +1800,15 @@ "core.submit": "Submit", "core.success": "Success", "core.tablet": "Tablet", + "core.tag.errorareanotsupported": "This tag area is not supported by the app.", + "core.tag.itemstaggedwith": "{{$a.tagarea}} tagged with \"{{$a.tag}}\"", + "core.tag.tag": "Tag", "core.tag.tagarea_course": "Courses", "core.tag.tagarea_course_modules": "Activities and resources", "core.tag.tagarea_post": "Blog posts", "core.tag.tagarea_user": "User interests", "core.tag.tags": "Tags", + "core.tag.warningareasnotsupported": "Some of the tag areas are not displayed because they are not supported by the app.", "core.teachers": "Teachers", "core.thereisdatatosync": "There are offline {{$a}} to be synchronised.", "core.thisdirection": "ltr", diff --git a/src/core/tag/lang/en.json b/src/core/tag/lang/en.json index 02e2888491d..b7f8b879434 100644 --- a/src/core/tag/lang/en.json +++ b/src/core/tag/lang/en.json @@ -1,7 +1,11 @@ { + "errorareanotsupported": "This tag area is not supported by the app.", + "itemstaggedwith": "{{$a.tagarea}} tagged with \"{{$a.tag}}\"", + "tag": "Tag", "tagarea_course": "Courses", "tagarea_course_modules": "Activities and resources", "tagarea_post": "Blog posts", "tagarea_user": "User interests", - "tags": "Tags" + "tags": "Tags", + "warningareasnotsupported": "Some of the tag areas are not displayed because they are not supported by the app." } diff --git a/src/core/tag/pages/index-area/index-area.html b/src/core/tag/pages/index-area/index-area.html new file mode 100644 index 00000000000..8a43d2d51b5 --- /dev/null +++ b/src/core/tag/pages/index-area/index-area.html @@ -0,0 +1,16 @@ + + + {{ 'core.tag.itemstaggedwith' | translate: { $a: {tagarea: areaNameKey | translate, tag: tagName} } }} + + + + + + + + + + + + + diff --git a/src/core/tag/pages/index-area/index-area.module.ts b/src/core/tag/pages/index-area/index-area.module.ts new file mode 100644 index 00000000000..87a49cd7fc3 --- /dev/null +++ b/src/core/tag/pages/index-area/index-area.module.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreTagIndexAreaPage } from './index-area'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + CoreTagIndexAreaPage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(CoreTagIndexAreaPage), + TranslateModule.forChild() + ], +}) +export class CoreTagIndexAreaPageModule {} diff --git a/src/core/tag/pages/index-area/index-area.ts b/src/core/tag/pages/index-area/index-area.ts new file mode 100644 index 00000000000..9f71f053287 --- /dev/null +++ b/src/core/tag/pages/index-area/index-area.ts @@ -0,0 +1,150 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTagProvider } from '@core/tag/providers/tag'; +import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate'; + +/** + * Page that displays the tag index area. + */ +@IonicPage({ segment: 'core-tag-index-area' }) +@Component({ + selector: 'page-core-tag-index-area', + templateUrl: 'index-area.html', +}) +export class CoreTagIndexAreaPage { + tagId: number; + tagName: string; + collectionId: number; + areaId: number; + fromContextId: number; + contextId: number; + recursive: boolean; + areaNameKey: string; + loaded = false; + componentName: string; + itemType: string; + items = []; + nextPage = 0; + canLoadMore = false; + areaComponent: any; + loadMoreError = false; + + constructor(navParams: NavParams, private injector: Injector, private translate: TranslateService, + private tagProvider: CoreTagProvider, private domUtils: CoreDomUtilsProvider, + private tagAreaDelegate: CoreTagAreaDelegate) { + this.tagId = navParams.get('tagId'); + this.tagName = navParams.get('tagName'); + this.collectionId = navParams.get('collectionId'); + this.areaId = navParams.get('areaId'); + this.fromContextId = navParams.get('fromContextId'); + this.contextId = navParams.get('contextId'); + this.recursive = navParams.get('recursive'); + this.areaNameKey = navParams.get('areaNameKey'); + + // Pass the the following parameters to avoid fetching the first page. + this.componentName = navParams.get('componentName'); + this.itemType = navParams.get('itemType'); + this.items = navParams.get('items') || []; + this.nextPage = navParams.get('nextPage') || 0; + this.canLoadMore = !!navParams.get('canLoadMore'); + } + + /** + * View loaded. + */ + ionViewDidLoad(): void { + let promise: Promise; + if (!this.componentName || !this.itemType || !this.items.length || this.nextPage == 0) { + promise = this.fetchData(true); + } else { + promise = Promise.resolve(); + } + + promise.then(() => { + return this.tagAreaDelegate.getComponent(this.componentName, this.itemType, this.injector).then((component) => { + this.areaComponent = component; + }); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Fetch next page of the tag index area. + * + * @param {boolean} [refresh=false] Whether to refresh the data or fetch a new page. + * @return {Promise} Resolved when done. + */ + fetchData(refresh: boolean = false): Promise { + this.loadMoreError = false; + const page = refresh ? 0 : this.nextPage; + + return this.tagProvider.getTagIndexPerArea(this.tagId, this.tagName, this.collectionId, this.areaId, this.fromContextId, + this.contextId, this.recursive, page).then((areas) => { + const area = areas[0]; + + return this.tagAreaDelegate.parseContent(area.component, area.itemtype, area.content).then((items) => { + if (!items || !items.length) { + // Tag area not supported. + return Promise.reject(this.translate.instant('core.tag.errorareanotsupported')); + } + + if (page == 0) { + this.items = items; + } else { + this.items.push(...items); + } + this.componentName = area.component; + this.itemType = area.itemtype; + this.areaNameKey = this.tagAreaDelegate.getDisplayNameKey(area.component, area.itemtype); + this.canLoadMore = !!area.nextpageurl; + this.nextPage = page + 1; + }); + }).catch((error) => { + this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. + this.domUtils.showErrorModalDefault(error, 'Error loading tag index'); + }); + } + + /** + * Load more items. + * + * @param {any} infiniteComplete Infinite scroll complete function. + * @return {Promise} Resolved when done. + */ + loadMore(infiniteComplete: any): Promise { + return this.fetchData().finally(() => { + infiniteComplete(); + }); + } + + /** + * Refresh data. + * + * @param {any} refresher Refresher. + */ + refreshData(refresher: any): void { + this.tagProvider.invalidateTagIndexPerArea(this.tagId, this.tagName, this.collectionId, this.areaId, this.fromContextId, + this.contextId, this.recursive).finally(() => { + this.fetchData(true).finally(() => { + refresher.complete(); + }); + }); + } +} diff --git a/src/core/tag/pages/index/index.html b/src/core/tag/pages/index/index.html new file mode 100644 index 00000000000..5174fcd7cab --- /dev/null +++ b/src/core/tag/pages/index/index.html @@ -0,0 +1,24 @@ + + + {{ 'core.tag.tag' | translate }}: {{ tagName }} + + + + + + + + + + + + {{ 'core.tag.warningareasnotsupported' | translate }} + + +

{{ area.nameKey | translate }}

+ {{ area.badge }} +
+
+
+
+
diff --git a/src/core/tag/pages/index/index.module.ts b/src/core/tag/pages/index/index.module.ts new file mode 100644 index 00000000000..bb3cd138d18 --- /dev/null +++ b/src/core/tag/pages/index/index.module.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreTagIndexPage } from './index'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + CoreTagIndexPage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(CoreTagIndexPage), + TranslateModule.forChild() + ], +}) +export class CoreTagIndexPageModule {} diff --git a/src/core/tag/pages/index/index.ts b/src/core/tag/pages/index/index.ts new file mode 100644 index 00000000000..9185bae0fc1 --- /dev/null +++ b/src/core/tag/pages/index/index.ts @@ -0,0 +1,154 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, ViewChild } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreTagProvider } from '@core/tag/providers/tag'; +import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate'; + +/** + * Page that displays the tag index. + */ +@IonicPage({ segment: 'core-tag-index' }) +@Component({ + selector: 'page-core-tag-index', + templateUrl: 'index.html', +}) +export class CoreTagIndexPage { + @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; + + tagId: number; + tagName: string; + collectionId: number; + areaId: number; + fromContextId: number; + contextId: number; + recursive: boolean; + loaded = false; + areas: Array<{ + id: number, + componentName: string, + itemType: string, + nameKey: string, + items: any[], + canLoadMore: boolean, + badge: string + }>; + selectedAreaId: number; + hasUnsupportedAreas = false; + + constructor(navParams: NavParams, private tagProvider: CoreTagProvider, private domUtils: CoreDomUtilsProvider, + private tagAreaDelegate: CoreTagAreaDelegate) { + this.tagId = navParams.get('tagId') || 0; + this.tagName = navParams.get('tagName') || ''; + this.collectionId = navParams.get('collectionId'); + this.areaId = navParams.get('areaId') || 0; + this.fromContextId = navParams.get('fromContextId') || 0; + this.contextId = navParams.get('contextId') || 0; + this.recursive = navParams.get('recursive') || true; + } + + /** + * View loaded. + */ + ionViewDidLoad(): void { + this.fetchData().then(() => { + if (this.splitviewCtrl.isOn() && this.areas && this.areas.length > 0) { + const area = this.areas.find((area) => area.id == this.areaId); + this.openArea(area || this.areas[0]); + } + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Fetch first page of tag index per area. + * + * @return {Promise} Resolved when done. + */ + fetchData(): Promise { + return this.tagProvider.getTagIndexPerArea(this.tagId, this.tagName, this.collectionId, this.areaId, this.fromContextId, + this.contextId, this.recursive, 0).then((areas) => { + this.areas = []; + this.hasUnsupportedAreas = false; + + return Promise.all(areas.map((area) => { + return this.tagAreaDelegate.parseContent(area.component, area.itemtype, area.content).then((items) => { + if (!items || !items.length) { + // Tag area not supported, skip. + this.hasUnsupportedAreas = true; + + return null; + } + + return { + id: area.ta, + componentName: area.component, + itemType: area.itemtype, + nameKey: this.tagAreaDelegate.getDisplayNameKey(area.component, area.itemtype), + items, + canLoadMore: !!area.nextpageurl, + badge: items && items.length ? items.length + (area.nextpageurl ? '+' : '') : '', + }; + }); + })).then((areas) => { + this.areas = areas.filter((area) => area != null); + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error loading tag index'); + }); + } + + /** + * Refresh data. + * + * @param {any} refresher Refresher. + */ + refreshData(refresher: any): void { + this.tagProvider.invalidateTagIndexPerArea(this.tagId, this.tagName, this.collectionId, this.areaId, this.fromContextId, + this.contextId, this.recursive).finally(() => { + this.fetchData().finally(() => { + refresher.complete(); + }); + }); + } + + /** + * Navigate to an index area. + * + * @param {any} area Area. + */ + openArea(area: any): void { + this.selectedAreaId = area.id; + const params = { + tagId: this.tagId, + tagName: this.tagName, + collectionId: this.collectionId, + areaId: area.id, + fromContextId: this.fromContextId, + contextId: this.contextId, + recursive: this.recursive, + areaNameKey: area.nameKey, + componentName: area.component, + itemType: area.itemType, + items: area.items.slice(), + canLoadMore: area.canLoadMore, + nextPage: 1 + }; + this.splitviewCtrl.push('CoreTagIndexAreaPage', params); + } +} diff --git a/src/core/tag/providers/tag.ts b/src/core/tag/providers/tag.ts index fdcdaf189a2..88b739f04b2 100644 --- a/src/core/tag/providers/tag.ts +++ b/src/core/tag/providers/tag.ts @@ -13,8 +13,27 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; import { CoreSitesProvider } from '@providers/sites'; -import { CoreSite } from '@classes/site'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; + +/** + * Structure of a tag index returned by WS. + */ +export interface CoreTagIndex { + tagid: number; + ta: number; + component: string; + itemtype: string; + nextpageurl: string; + prevpageurl: string; + exclusiveurl: string; + exclusivetext: string; + title: string; + content: string; + hascontent: number; + anchor: string; +} /** * Structure of a tag item returned by WS. @@ -38,7 +57,9 @@ export interface CoreTagItem { @Injectable() export class CoreTagProvider { - constructor(private sitesProvider: CoreSitesProvider) {} + protected ROOT_CACHE_KEY = 'CoreTag:'; + + constructor(private sitesProvider: CoreSitesProvider, private translate: TranslateService) {} /** * Check whether tags are available in a certain site. @@ -67,4 +88,96 @@ export class CoreTagProvider { site.wsAvailable('core_tag_get_tag_collections') && !site.isFeatureDisabled('NoDelegate_CoreTag'); } + + /** + * Fetch the tag index. + * + * @param {number} [id=0] Tag ID. + * @param {string} [name=''] Tag name. + * @param {number} [collectionId=0] Tag collection ID. + * @param {number} [areaId=0] Tag area ID. + * @param {number} [fromContextId=0] Context ID where the link was displayed. + * @param {number} [contextId=0] Context ID where to search for items. + * @param {boolean} [recursive=true] Search in the context and its children. + * @param {number} [page=0] Page number. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the tag index per area. + * @since 3.7 + */ + getTagIndexPerArea(id: number, name: string = '', collectionId: number = 0, areaId: number = 0, fromContextId: number = 0, + contextId: number = 0, recursive: boolean = true, page: number = 0, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + tagindex: { + id: id, + tag: name, + tc: collectionId, + ta: areaId, + excl: true, + from: fromContextId, + ctx: contextId, + rec: recursive, + page: page + }, + }; + const preSets: CoreSiteWSPreSets = { + updateFrequency: CoreSite.FREQUENCY_OFTEN, + cacheKey: this.getTagIndexPerAreaKey(id, name, collectionId, areaId, fromContextId, contextId, recursive) + }; + + return site.read('core_tag_get_tagindex_per_area', params, preSets).catch((error) => { + // Workaround for WS not passing parameter to error string. + if (error && error.errorcode == 'notagsfound') { + error.message = this.translate.instant('core.tag.notagsfound', {$a: name || id || ''}); + } + + return Promise.reject(error); + }).then((response) => { + if (!response || !response.length) { + return Promise.reject(null); + } + + return response; + }); + }); + } + + /** + * Invalidate tag index. + * + * @param {number} [id=0] Tag ID. + * @param {string} [name=''] Tag name. + * @param {number} [collectionId=0] Tag collection ID. + * @param {number} [areaId=0] Tag area ID. + * @param {number} [fromContextId=0] Context ID where the link was displayed. + * @param {number} [contextId=0] Context ID where to search for items. + * @param {boolean} [recursive=true] Search in the context and its children. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateTagIndexPerArea(id: number, name: string = '', collectionId: number = 0, areaId: number = 0, + fromContextId: number = 0, contextId: number = 0, recursive: boolean = true, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const key = this.getTagIndexPerAreaKey(id, name, collectionId, areaId, fromContextId, contextId, recursive); + + return site.invalidateWsCacheForKey(key); + }); + } + + /** + * Get cache key for tag index. + * + * @param {number} id Tag ID. + * @param {string} name Tag name. + * @param {number} collectionId Tag collection ID. + * @param {number} areaId Tag area ID. + * @param {number} fromContextId Context ID where the link was displayed. + * @param {number} contextId Context ID where to search for items. + * @param {boolean} [recursive=true] Search in the context and its children. + * @return {string} Cache key. + */ + protected getTagIndexPerAreaKey(id: number, name: string, collectionId: number, areaId: number, fromContextId: number, + contextId: number, recursive: boolean): string { + return this.ROOT_CACHE_KEY + 'index:' + id + ':' + name + ':' + collectionId + ':' + areaId + ':' + fromContextId + ':' + + contextId + ':' + (recursive ? 1 : 0); + } } From b5b480e226c5aaa0984a133706ff932b31ae456f Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 17 Jul 2019 16:13:00 +0200 Subject: [PATCH 102/241] MOBILE-2201 search-box: Initial search text property --- src/components/search-box/search-box.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/search-box/search-box.ts b/src/components/search-box/search-box.ts index 61922cbb353..e978908d163 100644 --- a/src/components/search-box/search-box.ts +++ b/src/components/search-box/search-box.ts @@ -39,6 +39,7 @@ export class CoreSearchBoxComponent implements OnInit { @Input() lengthCheck = 3; // Check value length before submit. If 0, any string will be submitted. @Input() showClear = true; // Show/hide clear button. @Input() disabled = false; // Disables the input text. + @Input() initialSearch: string; // Initial search text. @Output() onSubmit: EventEmitter; // Send data when submitting the search form. @Output() onClear: EventEmitter; // Send event when clearing the search form. @@ -55,6 +56,7 @@ export class CoreSearchBoxComponent implements OnInit { this.placeholder = this.placeholder || this.translate.instant('core.search'); this.spellcheck = this.utils.isTrueOrOne(this.spellcheck); this.showClear = this.utils.isTrueOrOne(this.showClear); + this.searchText = this.initialSearch || ''; } /** From d00d40189417fcc1156cc3b75749c848468edaa2 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 12:35:47 +0200 Subject: [PATCH 103/241] MOBILE-2201 tag: Search page --- scripts/langindex.json | 5 + src/assets/lang/en.json | 5 + src/core/tag/lang/en.json | 5 + src/core/tag/pages/search/search.html | 37 +++++ src/core/tag/pages/search/search.module.ts | 33 +++++ src/core/tag/pages/search/search.scss | 95 ++++++++++++ src/core/tag/pages/search/search.ts | 135 +++++++++++++++++ src/core/tag/providers/mainmenu-handler.ts | 59 ++++++++ src/core/tag/providers/tag.ts | 162 +++++++++++++++++++++ src/core/tag/tag.module.ts | 9 +- 10 files changed, 544 insertions(+), 1 deletion(-) create mode 100644 src/core/tag/pages/search/search.html create mode 100644 src/core/tag/pages/search/search.module.ts create mode 100644 src/core/tag/pages/search/search.scss create mode 100644 src/core/tag/pages/search/search.ts create mode 100644 src/core/tag/providers/mainmenu-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 679e4d55931..10ac601e4a2 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1800,8 +1800,13 @@ "core.submit": "moodle", "core.success": "moodle", "core.tablet": "local_moodlemobileapp", + "core.tag.defautltagcoll": "moodle", "core.tag.errorareanotsupported": "local_moodlemobileapp", + "core.tag.inalltagcoll": "moodle", "core.tag.itemstaggedwith": "moodle", + "core.tag.notagsfound": "moodle", + "core.tag.searchtags": "moodle", + "core.tag.showingfirsttags": "moodle", "core.tag.tag": "moodle", "core.tag.tagarea_course": "moodle", "core.tag.tagarea_course_modules": "moodle", diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 69730eab94f..726ea14f23c 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1800,8 +1800,13 @@ "core.submit": "Submit", "core.success": "Success", "core.tablet": "Tablet", + "core.tag.defautltagcoll": "Default collection", "core.tag.errorareanotsupported": "This tag area is not supported by the app.", + "core.tag.inalltagcoll": "Everywhere", "core.tag.itemstaggedwith": "{{$a.tagarea}} tagged with \"{{$a.tag}}\"", + "core.tag.notagsfound": "No tags matching \"{{$a}}\" found", + "core.tag.searchtags": "Search tags", + "core.tag.showingfirsttags": "Showing {{$a}} most popular tags", "core.tag.tag": "Tag", "core.tag.tagarea_course": "Courses", "core.tag.tagarea_course_modules": "Activities and resources", diff --git a/src/core/tag/lang/en.json b/src/core/tag/lang/en.json index b7f8b879434..c23afc5e934 100644 --- a/src/core/tag/lang/en.json +++ b/src/core/tag/lang/en.json @@ -1,6 +1,11 @@ { + "defautltagcoll": "Default collection", "errorareanotsupported": "This tag area is not supported by the app.", + "inalltagcoll": "Everywhere", "itemstaggedwith": "{{$a.tagarea}} tagged with \"{{$a.tag}}\"", + "notagsfound": "No tags matching \"{{$a}}\" found", + "searchtags": "Search tags", + "showingfirsttags": "Showing {{$a}} most popular tags", "tag": "Tag", "tagarea_course": "Courses", "tagarea_course_modules": "Activities and resources", diff --git a/src/core/tag/pages/search/search.html b/src/core/tag/pages/search/search.html new file mode 100644 index 00000000000..5635d09d934 --- /dev/null +++ b/src/core/tag/pages/search/search.html @@ -0,0 +1,37 @@ + + + {{ 'core.tag.searchtags' | translate }} + + + + + + + + + + + + + + {{ 'core.tag.inalltagcoll' | translate }} + {{ collection.name }} + + + + + + + + +
+ + {{ tag.name }} + +
+

+ {{ 'core.tag.showingfirsttags' | translate: {$a: cloud.tags.length} }} +

+
+
+
diff --git a/src/core/tag/pages/search/search.module.ts b/src/core/tag/pages/search/search.module.ts new file mode 100644 index 00000000000..29776ce6810 --- /dev/null +++ b/src/core/tag/pages/search/search.module.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreTagSearchPage } from './search'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + CoreTagSearchPage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(CoreTagSearchPage), + TranslateModule.forChild() + ], +}) +export class CoreTagSerchPageModule {} diff --git a/src/core/tag/pages/search/search.scss b/src/core/tag/pages/search/search.scss new file mode 100644 index 00000000000..cd71734457c --- /dev/null +++ b/src/core/tag/pages/search/search.scss @@ -0,0 +1,95 @@ +ion-app.app-root page-core-tag-search { + core-search-box ion-card { + width: 100% !important; + margin: 0 !important; + } + + .core-tag-cloud ion-badge { + margin: 8px; + cursor: pointer; + + .size20 { + font-size: 3.4rem; + } + + .size19 { + font-size: 3.3rem; + } + + .size18 { + font-size: 3.2rem; + } + + .size17 { + font-size: 3.1rem; + } + + .size16 { + font-size: 3rem; + } + + .size15 { + font-size: 2.9rem; + } + + .size14 { + font-size: 2.8rem; + } + + .size13 { + font-size: 2.7rem; + } + + .size12 { + font-size: 2.6rem; + } + + .size11 { + font-size: 2.5rem; + } + + .size10 { + font-size: 2.4rem; + } + + .size9 { + font-size: 2.3rem; + } + + .size8 { + font-size: 2.2rem; + } + + .size7 { + font-size: 2.1rem; + } + + .size6 { + font-size: 2rem; + } + + .size5 { + font-size: 1.9rem; + } + + .size4 { + font-size: 1.8rem; + } + + .size3 { + font-size: 1.7rem; + } + + .size2 { + font-size: 1.6rem; + } + + .size1 { + font-size: 1.5rem; + } + + .size0 { + font-size: 1.4rem; + } + } +} diff --git a/src/core/tag/pages/search/search.ts b/src/core/tag/pages/search/search.ts new file mode 100644 index 00000000000..13f09bb4cec --- /dev/null +++ b/src/core/tag/pages/search/search.ts @@ -0,0 +1,135 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, NavParams, NavController } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreAppProvider } from '@providers/app'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { CoreTagProvider, CoreTagCloud, CoreTagCollection, CoreTagCloudTag } from '@core/tag/providers/tag'; + +/** + * Page that displays most used tags and allows searching. + */ +@IonicPage({ segment: 'core-tag-search' }) +@Component({ + selector: 'page-core-tag-search', + templateUrl: 'search.html', +}) +export class CoreTagSearchPage { + collectionId: number; + query: string; + collections: CoreTagCollection[] = []; + cloud: CoreTagCloud; + loaded = false; + searching = false; + + constructor(private navCtrl: NavController, navParams: NavParams, private appProvider: CoreAppProvider, + private translate: TranslateService, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider, + private textUtils: CoreTextUtilsProvider, private contentLinksHelper: CoreContentLinksHelperProvider, + private tagProvider: CoreTagProvider) { + this.collectionId = navParams.get('collectionId') || 0; + this.query = navParams.get('query') || ''; + } + + /** + * View loaded. + */ + ionViewDidLoad(): void { + this.fetchData().finally(() => { + this.loaded = true; + }); + } + + fetchData(): Promise { + return Promise.all([ + this.fetchCollections(), + this.fetchTags() + ]).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error loading tags.'); + }); + } + + /** + * Fetch tag collections. + * + * @return {Promise} Resolved when done. + */ + fetchCollections(): Promise { + return this.tagProvider.getTagCollections().then((collections) => { + collections.forEach((collection) => { + if (!collection.name && collection.isdefault) { + collection.name = this.translate.instant('core.tag.defautltagcoll'); + } + }); + this.collections = collections; + }); + } + + /** + * Fetch tags. + * + * @return {Promise} Resolved when done. + */ + fetchTags(): Promise { + return this.tagProvider.getTagCloud(this.collectionId, undefined, undefined, this.query).then((cloud) => { + this.cloud = cloud; + }); + } + + /** + * Go to tag index page. + */ + openTag(tag: CoreTagCloudTag): void { + const url = this.textUtils.decodeURI(tag.viewurl); + this.contentLinksHelper.handleLink(url, undefined, this.navCtrl); + } + + /** + * Refresh data. + * + * @param {any} refresher Refresher. + */ + refreshData(refresher: any): void { + this.utils.allPromises([ + this.tagProvider.invalidateTagCollections(), + this.tagProvider.invalidateTagCloud(this.collectionId, undefined, undefined, this.query), + ]).finally(() => { + return this.fetchData().finally(() => { + refresher.complete(); + }); + }); + } + + /** + * Search tags. + * + * @param {string} query Search query. + * @return {Promise} Resolved when done. + */ + searchTags(query: string): Promise { + this.searching = true; + this.query = query; + this.appProvider.closeKeyboard(); + + return this.fetchTags().catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error loading tags.'); + }).finally(() => { + this.searching = false; + }); + } +} diff --git a/src/core/tag/providers/mainmenu-handler.ts b/src/core/tag/providers/mainmenu-handler.ts new file mode 100644 index 00000000000..8e676acc1d5 --- /dev/null +++ b/src/core/tag/providers/mainmenu-handler.ts @@ -0,0 +1,59 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreTagProvider } from './tag'; +import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/providers/delegate'; +import { CoreUtilsProvider } from '@providers/utils/utils'; + +/** + * Handler to inject an option into main menu. + */ +@Injectable() +export class CoreTagMainMenuHandler implements CoreMainMenuHandler { + name = 'CoreTag'; + priority = 300; + + constructor(private tagProvider: CoreTagProvider, private utils: CoreUtilsProvider) { } + + /** + * Check if the handler is enabled on a site level. + * + * @return {boolean | Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return this.tagProvider.areTagsAvailable().then((available) => { + if (!available) { + return false; + } + + // The only way to check whether tags are enabled on web is to perform a WS call. + return this.utils.promiseWorks(this.tagProvider.getTagCollections()); + }); + } + + /** + * Returns the data needed to render the handler. + * + * @return {CoreMainMenuHandlerData} Data needed to render the handler. + */ + getDisplayData(): CoreMainMenuHandlerData { + return { + icon: 'pricetags', + title: 'core.tag.tags', + page: 'CoreTagSearchPage', + class: 'core-tag-search-handler' + }; + } +} diff --git a/src/core/tag/providers/tag.ts b/src/core/tag/providers/tag.ts index 88b739f04b2..3a377cba96a 100644 --- a/src/core/tag/providers/tag.ts +++ b/src/core/tag/providers/tag.ts @@ -17,6 +17,40 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +/** + * Structure of a tag cloud returned by WS. + */ +export interface CoreTagCloud { + tags: CoreTagCloudTag[]; + tagscount: number; + totalcount: number; +} + +/** + * Structure of a tag cloud tag returned by WS. + */ +export interface CoreTagCloudTag { + name: string; + viewurl: string; + flag: boolean; + isstandard: boolean; + count: number; + size: number; +} + +/** + * Structure of a tag collection returned by WS. + */ +export interface CoreTagCollection { + id: number; + name: string; + isdefault: boolean; + component: string; + sortoder: number; + searchable: boolean; + customurl: string; +} + /** * Structure of a tag index returned by WS. */ @@ -57,6 +91,8 @@ export interface CoreTagItem { @Injectable() export class CoreTagProvider { + static SEARCH_LIMIT = 150; + protected ROOT_CACHE_KEY = 'CoreTag:'; constructor(private sitesProvider: CoreSitesProvider, private translate: TranslateService) {} @@ -89,6 +125,71 @@ export class CoreTagProvider { !site.isFeatureDisabled('NoDelegate_CoreTag'); } + /** + * Fetch the tag cloud. + * + * @param {number} [collectionId=0] Tag collection ID. + * @param {boolean} [isStandard=false] Whether to return only standard tags. + * @param {string} [sort='name'] Sort order for display (id, name, rawname, count, flag, isstandard, tagcollid). + * @param {string} [search=''] Search string. + * @param {number} [fromContextId=0] Context ID where this tag cloud is displayed. + * @param {number} [contextId=0] Only retrieve tag instances in this context. + * @param {boolean} [recursive=true] Retrieve tag instances in the context and its children. + * @param {number} [limit] Maximum number of tags to retrieve. Defaults to SEARCH_LIMIT. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the tag cloud. + * @since 3.7 + */ + getTagCloud(collectionId: number = 0, isStandard: boolean = false, sort: string = 'name', search: string = '', + fromContextId: number = 0, contextId: number = 0, recursive: boolean = true, limit?: number, siteId?: string): + Promise { + limit = limit || CoreTagProvider.SEARCH_LIMIT; + + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + tagcollid: collectionId, + isstandard: isStandard, + limit: limit, + sort: sort, + search: search, + fromctx: fromContextId, + ctx: contextId, + rec: recursive + }; + const preSets: CoreSiteWSPreSets = { + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + cacheKey: this.getTagCloudKey(collectionId, isStandard, sort, search, fromContextId, contextId, recursive), + getFromCache: search != '' // Try to get updated data when searching. + }; + + return site.read('core_tag_get_tag_cloud', params, preSets); + }); + } + + /** + * Fetch the tag collections. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the tag collections. + * @since 3.7 + */ + getTagCollections(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const preSets: CoreSiteWSPreSets = { + updateFrequency: CoreSite.FREQUENCY_RARELY, + cacheKey: this.getTagCollectionsKey() + }; + + return site.read('core_tag_get_tag_collections', null, preSets).then((response) => { + if (!response || !response.collections) { + return Promise.reject(null); + } + + return response.collections; + }); + }); + } + /** * Fetch the tag index. * @@ -142,6 +243,40 @@ export class CoreTagProvider { }); } + /** + * Invalidate tag cloud. + * + * @param {number} [collectionId=0] Tag collection ID. + * @param {boolean} [isStandard=false] Whether to return only standard tags. + * @param {string} [sort='name'] Sort order for display (id, name, rawname, count, flag, isstandard, tagcollid). + * @param {string} [search=''] Search string. + * @param {number} [fromContextId=0] Context ID where this tag cloud is displayed. + * @param {number} [contextId=0] Only retrieve tag instances in this context. + * @param {boolean} [recursive=true] Retrieve tag instances in the context and its children. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateTagCloud(collectionId: number = 0, isStandard: boolean = false, sort: string = 'name', search: string = '', + fromContextId: number = 0, contextId: number = 0, recursive: boolean = true, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const key = this.getTagCloudKey(collectionId, isStandard, sort, search, fromContextId, contextId, recursive); + + return site.invalidateWsCacheForKey(key); + }); + } + + /** + * Invalidate tag collections. + * + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateTagCollections(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const key = this.getTagCollectionsKey(); + + return site.invalidateWsCacheForKey(key); + }); + } + /** * Invalidate tag index. * @@ -163,6 +298,33 @@ export class CoreTagProvider { }); } + /** + * Get cache key for tag cloud. + * + * @param {number} collectionId Tag collection ID. + * @param {boolean} isStandard Whether to return only standard tags. + * @param {string} sort Sort order for display (id, name, rawname, count, flag, isstandard, tagcollid). + * @param {string} search Search string. + * @param {number} fromContextId Context ID where this tag cloud is displayed. + * @param {number} contextId Only retrieve tag instances in this context. + * @param {boolean} recursive Retrieve tag instances in the context and it's children. + * @return {string} Cache key. + */ + protected getTagCloudKey(collectionId: number, isStandard: boolean, sort: string, search: string, fromContextId: number, + contextId: number, recursive: boolean): string { + return this.ROOT_CACHE_KEY + 'cloud:' + collectionId + ':' + (isStandard ? 1 : 0) + ':' + sort + ':' + search + ':' + + fromContextId + ':' + contextId + ':' + (recursive ? 1 : 0); + } + + /** + * Get cache key for tag collections. + * + * @return {string} Cache key. + */ + protected getTagCollectionsKey(): string { + return this.ROOT_CACHE_KEY + 'collections'; + } + /** * Get cache key for tag index. * diff --git a/src/core/tag/tag.module.ts b/src/core/tag/tag.module.ts index 45d4d69af97..baa55d98f4f 100644 --- a/src/core/tag/tag.module.ts +++ b/src/core/tag/tag.module.ts @@ -13,9 +13,11 @@ // limitations under the License. import { NgModule } from '@angular/core'; +import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; import { CoreTagProvider } from './providers/tag'; import { CoreTagHelperProvider } from './providers/helper'; import { CoreTagAreaDelegate } from './providers/area-delegate'; +import { CoreTagMainMenuHandler } from './providers/mainmenu-handler'; @NgModule({ declarations: [ @@ -25,8 +27,13 @@ import { CoreTagAreaDelegate } from './providers/area-delegate'; providers: [ CoreTagProvider, CoreTagHelperProvider, - CoreTagAreaDelegate + CoreTagAreaDelegate, + CoreTagMainMenuHandler ] }) export class CoreTagModule { + + constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: CoreTagMainMenuHandler) { + mainMenuDelegate.registerHandler(mainMenuHandler); + } } From e4260aa92a7a698dc1ced0690c4888a3118ad1cf Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 12:37:05 +0200 Subject: [PATCH 104/241] MOBILE-2201 tag: Link handlers --- src/core/tag/providers/index-link-handler.ts | 81 +++++++++++++++++++ src/core/tag/providers/search-link-handler.ts | 70 ++++++++++++++++ src/core/tag/tag.module.ts | 13 ++- 3 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 src/core/tag/providers/index-link-handler.ts create mode 100644 src/core/tag/providers/search-link-handler.ts diff --git a/src/core/tag/providers/index-link-handler.ts b/src/core/tag/providers/index-link-handler.ts new file mode 100644 index 00000000000..c8e1d7c4977 --- /dev/null +++ b/src/core/tag/providers/index-link-handler.ts @@ -0,0 +1,81 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { CoreTagProvider } from './tag'; + +/** + * Handler to treat links to tag index. + */ +@Injectable() +export class CoreTagIndexLinkHandler extends CoreContentLinksHandlerBase { + name = 'CoreTagIndexLinkHandler'; + pattern = /\/tag\/index\.php/; + + constructor(private tagProvider: CoreTagProvider, private linkHelper: CoreContentLinksHelperProvider) { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @param {any} [data] Extra data to handle the URL. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number, data?: any): + CoreContentLinksAction[] | Promise { + return [{ + action: (siteId, navCtrl?): void => { + const pageParams = { + tagId: parseInt(params.id, 10) || 0, + tagName: params.tag || '', + collectionId: parseInt(params.tc, 10) || 0, + areaId: parseInt(params.ta, 10) || 0, + fromContextId: parseInt(params.from, 10) || 0, + contextId: parseInt(params.ctx, 10) || 0, + recursive: parseInt(params.rec, 10) || 1 + }; + + if (!pageParams.tagId && (!pageParams.tagName || !pageParams.collectionId)) { + this.linkHelper.goInSite(navCtrl, 'CoreTagSearchPage', {}, siteId); + } else if (pageParams.areaId) { + this.linkHelper.goInSite(navCtrl, 'CoreTagIndexAreaPage', pageParams, siteId); + } else { + this.linkHelper.goInSite(navCtrl, 'CoreTagIndexPage', pageParams, siteId); + } + } + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + return this.tagProvider.areTagsAvailable(siteId); + } +} diff --git a/src/core/tag/providers/search-link-handler.ts b/src/core/tag/providers/search-link-handler.ts new file mode 100644 index 00000000000..68ea7cb9696 --- /dev/null +++ b/src/core/tag/providers/search-link-handler.ts @@ -0,0 +1,70 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { CoreTagProvider } from './tag'; + +/** + * Handler to treat links to tag search. + */ +@Injectable() +export class CoreTagSearchLinkHandler extends CoreContentLinksHandlerBase { + name = 'CoreTagSearchLinkHandler'; + pattern = /\/tag\/search\.php/; + + constructor(private tagProvider: CoreTagProvider, private linkHelper: CoreContentLinksHelperProvider) { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @param {any} [data] Extra data to handle the URL. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number, data?: any): + CoreContentLinksAction[] | Promise { + return [{ + action: (siteId, navCtrl?): void => { + const pageParams = { + collectionId: parseInt(params.tc, 10) || 0, + query: params.query || '', + }; + + this.linkHelper.goInSite(navCtrl, 'CoreTagSearchPage', pageParams, siteId); + } + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + return this.tagProvider.areTagsAvailable(siteId); + } +} diff --git a/src/core/tag/tag.module.ts b/src/core/tag/tag.module.ts index baa55d98f4f..970e66e46b9 100644 --- a/src/core/tag/tag.module.ts +++ b/src/core/tag/tag.module.ts @@ -14,10 +14,13 @@ import { NgModule } from '@angular/core'; import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; +import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; import { CoreTagProvider } from './providers/tag'; import { CoreTagHelperProvider } from './providers/helper'; import { CoreTagAreaDelegate } from './providers/area-delegate'; import { CoreTagMainMenuHandler } from './providers/mainmenu-handler'; +import { CoreTagIndexLinkHandler } from './providers/index-link-handler'; +import { CoreTagSearchLinkHandler } from './providers/search-link-handler'; @NgModule({ declarations: [ @@ -28,12 +31,18 @@ import { CoreTagMainMenuHandler } from './providers/mainmenu-handler'; CoreTagProvider, CoreTagHelperProvider, CoreTagAreaDelegate, - CoreTagMainMenuHandler + CoreTagMainMenuHandler, + CoreTagIndexLinkHandler, + CoreTagSearchLinkHandler ] }) export class CoreTagModule { - constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: CoreTagMainMenuHandler) { + constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: CoreTagMainMenuHandler, + contentLinksDelegate: CoreContentLinksDelegate, indexLinkHandler: CoreTagIndexLinkHandler, + searchLinkHandler: CoreTagSearchLinkHandler) { mainMenuDelegate.registerHandler(mainMenuHandler); + contentLinksDelegate.registerHandler(indexLinkHandler); + contentLinksDelegate.registerHandler(searchLinkHandler); } } From 759f0c3624475a4547dfb85f70f553b13923167c Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 8 Jul 2019 15:30:57 +0200 Subject: [PATCH 105/241] MOBILE-2201 link: Fix URLs with escaped characters --- src/directives/format-text.ts | 2 +- src/directives/link.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index 489561674e9..5933f59c34f 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -380,7 +380,7 @@ export class CoreFormatTextDirective implements OnChanges { anchors.forEach((anchor) => { // Angular 2 doesn't let adding directives dynamically. Create the CoreLinkDirective manually. const linkDir = new CoreLinkDirective(anchor, this.domUtils, this.utils, this.sitesProvider, this.urlUtils, - this.contentLinksHelper, this.navCtrl, this.content, this.svComponent); + this.contentLinksHelper, this.navCtrl, this.content, this.svComponent, this.textUtils); linkDir.capture = true; linkDir.ngOnInit(); diff --git a/src/directives/link.ts b/src/directives/link.ts index 0540eb83faf..a10f69fded8 100644 --- a/src/directives/link.ts +++ b/src/directives/link.ts @@ -21,6 +21,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreConfigConstants } from '../configconstants'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; /** * Directive to open a link in external browser. @@ -41,7 +42,8 @@ export class CoreLinkDirective implements OnInit { constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider, private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider, private contentLinksHelper: CoreContentLinksHelperProvider, @Optional() private navCtrl: NavController, - @Optional() private content: Content, @Optional() private svComponent: CoreSplitViewComponent) { + @Optional() private content: Content, @Optional() private svComponent: CoreSplitViewComponent, + private textUtils: CoreTextUtilsProvider) { // This directive can be added dynamically. In that case, the first param is the anchor HTMLElement. this.element = element.nativeElement || element; } @@ -62,12 +64,13 @@ export class CoreLinkDirective implements OnInit { this.element.addEventListener('click', (event) => { // If the event prevented default action, do nothing. if (!event.defaultPrevented) { - const href = this.element.getAttribute('href'); + let href = this.element.getAttribute('href'); if (href) { event.preventDefault(); event.stopPropagation(); if (this.utils.isTrueOrOne(this.capture)) { + href = this.textUtils.decodeURI(href); this.contentLinksHelper.handleLink(href, undefined, navCtrl, true, true).then((treated) => { if (!treated) { this.navigate(href); From 1a641fee5bcbdca9912cb84b7ec2e7a44316529e Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 22 Jul 2019 08:48:57 +0200 Subject: [PATCH 106/241] MOBILE-3089 android: Target API 28 --- config.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.xml b/config.xml index f3db2044cca..211280ed847 100644 --- a/config.xml +++ b/config.xml @@ -23,7 +23,7 @@ - + From ecd918a0d73c5975b8288c605957e2ad10b4bae0 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 22 Jul 2019 13:05:36 +0200 Subject: [PATCH 107/241] MOBILE-3052 glossary: Don't prefetch get_by_id --- .../mod/glossary/components/index/index.ts | 2 +- src/addon/mod/glossary/providers/glossary.ts | 239 +++++++++++++++--- .../glossary/providers/prefetch-handler.ts | 17 +- src/classes/site.ts | 15 +- 4 files changed, 227 insertions(+), 46 deletions(-) diff --git a/src/addon/mod/glossary/components/index/index.ts b/src/addon/mod/glossary/components/index/index.ts index 558eac5311b..ed3d807e930 100644 --- a/src/addon/mod/glossary/components/index/index.ts +++ b/src/addon/mod/glossary/components/index/index.ts @@ -269,7 +269,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity this.viewMode = 'cat'; this.fetchFunction = this.glossaryProvider.getEntriesByCategory; this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByCategory; - this.fetchArguments = [this.glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATERGORIES]; + this.fetchArguments = [this.glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATEGORIES]; this.getDivider = (entry: any): string => entry.categoryname; this.showDivider = (entry?: any, previous?: any): boolean => { return !previous || this.getDivider(entry) != this.getDivider(previous); diff --git a/src/addon/mod/glossary/providers/glossary.ts b/src/addon/mod/glossary/providers/glossary.ts index bd6862d688a..cd494187f3b 100644 --- a/src/addon/mod/glossary/providers/glossary.ts +++ b/src/addon/mod/glossary/providers/glossary.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreSite } from '@classes/site'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; @@ -32,13 +32,40 @@ export class AddonModGlossaryProvider { static COMPONENT = 'mmaModGlossary'; static LIMIT_ENTRIES = 25; static LIMIT_CATEGORIES = 10; - static SHOW_ALL_CATERGORIES = 0; + static SHOW_ALL_CATEGORIES = 0; static SHOW_NOT_CATEGORISED = -1; static ADD_ENTRY_EVENT = 'addon_mod_glossary_add_entry'; protected ROOT_CACHE_KEY = 'mmaModGlossary:'; + // Variables for database. + static ENTRIES_TABLE = 'addon_mod_glossary_entry_glossaryid'; + protected siteSchema: CoreSiteSchema = { + name: 'AddonModGlossaryProvider', + version: 1, + tables: [ + { + name: AddonModGlossaryProvider.ENTRIES_TABLE, + columns: [ + { + name: 'entryid', + type: 'INTEGER', + primaryKey: true + }, + { + name: 'glossaryid', + type: 'INTEGER', + }, + { + name: 'pagefrom', + type: 'INTEGER', + } + ] + } + ] + }; + constructor(private appProvider: CoreAppProvider, private sitesProvider: CoreSitesProvider, private filepoolProvider: CoreFilepoolProvider, @@ -46,7 +73,10 @@ export class AddonModGlossaryProvider { private textUtils: CoreTextUtilsProvider, private utils: CoreUtilsProvider, private glossaryOffline: AddonModGlossaryOfflineProvider, - private logHelper: CoreCourseLogHelperProvider) {} + private logHelper: CoreCourseLogHelperProvider) { + + this.sitesProvider.registerSiteSchema(this.siteSchema); + } /** * Get the course glossary cache key. @@ -118,12 +148,13 @@ export class AddonModGlossaryProvider { * @param {string} sort The direction of the order: ASC or DESC * @param {number} from Start returning records from here. * @param {number} limit Number of records to return. - * @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false. + * @param {boolean} omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. + * @param {boolean} forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Resolved with the entries. */ getEntriesByAuthor(glossaryId: number, letter: string, field: string, sort: string, from: number, limit: number, - forceCache: boolean, siteId?: string): Promise { + omitExpires: boolean, forceOffline: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { id: glossaryId, @@ -135,7 +166,8 @@ export class AddonModGlossaryProvider { }; const preSets = { cacheKey: this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort), - omitExpires: forceCache, + omitExpires: omitExpires, + forceOffline: forceOffline, updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; @@ -165,16 +197,18 @@ export class AddonModGlossaryProvider { * Get entries by category. * * @param {number} glossaryId Glossary Id. - * @param {string} categoryId The category ID. Use constant SHOW_ALL_CATERGORIES for all categories, or + * @param {string} categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or * constant SHOW_NOT_CATEGORISED for uncategorised entries. * @param {number} from Start returning records from here. * @param {number} limit Number of records to return. - * @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false. + * @param {boolean} omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. + * @param {boolean} forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Resolved with the entries. */ - getEntriesByCategory(glossaryId: number, categoryId: number, from: number, limit: number, forceCache: boolean, - siteId?: string): Promise { + getEntriesByCategory(glossaryId: number, categoryId: number, from: number, limit: number, omitExpires: boolean, + forceOffline: boolean, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const params = { id: glossaryId, @@ -184,7 +218,8 @@ export class AddonModGlossaryProvider { }; const preSets = { cacheKey: this.getEntriesByCategoryCacheKey(glossaryId, categoryId), - omitExpires: forceCache, + omitExpires: omitExpires, + forceOffline: forceOffline, updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; @@ -196,7 +231,7 @@ export class AddonModGlossaryProvider { * Invalidate cache of entries by category. * * @param {number} glossaryId Glossary Id. - * @param {string} categoryId The category ID. Use constant SHOW_ALL_CATERGORIES for all categories, or + * @param {string} categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or * constant SHOW_NOT_CATEGORISED for uncategorised entries. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Resolved when data is invalidated. @@ -213,7 +248,7 @@ export class AddonModGlossaryProvider { * Get the entries by category cache key. * * @param {number} glossaryId Glossary Id. - * @param {string} categoryId The category ID. Use constant SHOW_ALL_CATERGORIES for all categories, or + * @param {string} categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or * constant SHOW_NOT_CATEGORISED for uncategorised entries. * @return {string} Cache key. */ @@ -241,12 +276,14 @@ export class AddonModGlossaryProvider { * @param {string} sort The direction of the order. * @param {number} from Start returning records from here. * @param {number} limit Number of records to return. - * @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false. + * @param {boolean} omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. + * @param {boolean} forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Resolved with the entries. */ - getEntriesByDate(glossaryId: number, order: string, sort: string, from: number, limit: number, forceCache: boolean, - siteId?: string): Promise { + getEntriesByDate(glossaryId: number, order: string, sort: string, from: number, limit: number, omitExpires: boolean, + forceOffline: boolean, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const params = { id: glossaryId, @@ -257,7 +294,8 @@ export class AddonModGlossaryProvider { }; const preSets = { cacheKey: this.getEntriesByDateCacheKey(glossaryId, order, sort), - omitExpires: forceCache, + omitExpires: omitExpires, + forceOffline: forceOffline, updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; @@ -300,12 +338,14 @@ export class AddonModGlossaryProvider { * @param {string} letter A letter, or a special keyword. * @param {number} from Start returning records from here. * @param {number} limit Number of records to return. - * @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false. + * @param {boolean} omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. + * @param {boolean} forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Resolved with the entries. + * @return {Promise} Resolved with the entries. */ - getEntriesByLetter(glossaryId: number, letter: string, from: number, limit: number, forceCache: boolean, siteId?: string): - Promise { + getEntriesByLetter(glossaryId: number, letter: string, from: number, limit: number, omitExpires: boolean, forceOffline: boolean, + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const params = { id: glossaryId, @@ -315,11 +355,22 @@ export class AddonModGlossaryProvider { }; const preSets = { cacheKey: this.getEntriesByLetterCacheKey(glossaryId, letter), - omitExpires: forceCache, + omitExpires: omitExpires, + forceOffline: forceOffline, updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; - return site.read('mod_glossary_get_entries_by_letter', params, preSets); + return site.read('mod_glossary_get_entries_by_letter', params, preSets).then((result) => { + + if (limit == AddonModGlossaryProvider.LIMIT_ENTRIES) { + // Store entries in background, don't block the user for this. + this.storeEntries(glossaryId, result.entries, from, site.getId()).catch(() => { + // Ignore errors. + }); + } + + return result; + }); }); } @@ -364,12 +415,13 @@ export class AddonModGlossaryProvider { * @param {string} sort The direction of the order. * @param {number} from Start returning records from here. * @param {number} limit Number of records to return. - * @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false. + * @param {boolean} omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. + * @param {boolean} forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Resolved with the entries. */ getEntriesBySearch(glossaryId: number, query: string, fullSearch: boolean, order: string, sort: string, from: number, - limit: number, forceCache: boolean, siteId?: string): Promise { + limit: number, omitExpires: boolean, forceOffline: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { id: glossaryId, @@ -382,7 +434,8 @@ export class AddonModGlossaryProvider { }; const preSets = { cacheKey: this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch, order, sort), - omitExpires: forceCache, + omitExpires: omitExpires, + forceOffline: forceOffline, updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; @@ -497,7 +550,7 @@ export class AddonModGlossaryProvider { * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the entry. */ - getEntry(entryId: number, siteId?: string): Promise<{entry: any, ratinginfo: CoreRatingInfo}> { + getEntry(entryId: number, siteId?: string): Promise<{entry: any, ratinginfo: CoreRatingInfo, from?: number}> { return this.sitesProvider.getSite(siteId).then((site) => { const params = { id: entryId @@ -513,6 +566,74 @@ export class AddonModGlossaryProvider { } else { return Promise.reject(null); } + }).catch((error) => { + // Entry not found. Search it in the list of entries. + let glossaryId; + + const searchEntry = (from: number, loadNext: boolean): Promise => { + // Get the entries from this "page" and check if the entry we're looking for is in it. + return this.getEntriesByLetter(glossaryId, 'ALL', from, AddonModGlossaryProvider.LIMIT_ENTRIES, false, true, + siteId).then((result) => { + + for (let i = 0; i < result.entries.length; i++) { + const entry = result.entries[i]; + if (entry.id == entryId) { + // Entry found, return it. + return { + entry: entry, + from: from + }; + } + } + + const nextFrom = from + result.entries.length; + if (nextFrom < result.count && loadNext) { + // Get the next "page". + return searchEntry(nextFrom, true); + } + + // No more pages and the entry wasn't found. Reject. + return Promise.reject(null); + }); + }; + + return this.getStoredDataForEntry(entryId, site.getId()).then((data) => { + glossaryId = data.glossaryId; + + if (typeof data.from != 'undefined') { + return searchEntry(data.from, false).catch(() => { + // Entry not found in that page. Search all pages. + return searchEntry(0, true); + }); + } + + // Page not specified, search all pages. + return searchEntry(0, true); + }).catch(() => { + return Promise.reject(error); + }); + }); + }); + } + + /** + * Get a glossary ID and the "from" of a given entry. + * + * @param {number} entryId Entry ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the glossary ID and the "from". + */ + getStoredDataForEntry(entryId: number, siteId?: string): Promise<{glossaryId: number, from: number}> { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + entryid: entryId + }; + + return site.getDb().getRecord(AddonModGlossaryProvider.ENTRIES_TABLE, conditions).then((record) => { + return { + glossaryId: record.glossaryid, + from: record.pagefrom + }; }); }); } @@ -524,19 +645,21 @@ export class AddonModGlossaryProvider { * @param {any[]} fetchArguments Arguments to call the fetching. * @param {number} [limitFrom=0] Number of entries already fetched, so fetch will be done from this number. * @param {number} [limitNum] Number of records to return. Defaults to LIMIT_ENTRIES. - * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [omitExpires=false] True to always get the value from cache. If data isn't cached, it will call the WS. + * @param {boolean} [forceOffline=false] True to always get the value from cache. If data isn't cached, it won't call the WS. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the response. */ fetchEntries(fetchFunction: Function, fetchArguments: any[], limitFrom: number = 0, limitNum?: number, - forceCache: boolean = false, siteId?: string): Promise { + omitExpires: boolean = false, forceOffline: boolean = false, siteId?: string): Promise { limitNum = limitNum || AddonModGlossaryProvider.LIMIT_ENTRIES; siteId = siteId || this.sitesProvider.getCurrentSiteId(); const args = fetchArguments.slice(); args.push(limitFrom); args.push(limitNum); - args.push(forceCache); + args.push(omitExpires); + args.push(forceOffline); args.push(siteId); return fetchFunction.apply(this, args); @@ -547,18 +670,21 @@ export class AddonModGlossaryProvider { * * @param {Function} fetchFunction Function to fetch. * @param {any[]} fetchArguments Arguments to call the fetching. - * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [omitExpires=false] True to always get the value from cache. If data isn't cached, it will call the WS. + * @param {boolean} [forceOffline=false] True to always get the value from cache. If data isn't cached, it won't call the WS. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with all entrries. */ - fetchAllEntries(fetchFunction: Function, fetchArguments: any[], forceCache: boolean = false, siteId?: string): Promise { + fetchAllEntries(fetchFunction: Function, fetchArguments: any[], omitExpires: boolean = false, forceOffline: boolean = false, + siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); const entries = []; const limitNum = AddonModGlossaryProvider.LIMIT_ENTRIES; const fetchMoreEntries = (): Promise => { - return this.fetchEntries(fetchFunction, fetchArguments, entries.length, limitNum, forceCache, siteId).then((result) => { + return this.fetchEntries(fetchFunction, fetchArguments, entries.length, limitNum, omitExpires, forceOffline, siteId) + .then((result) => { Array.prototype.push.apply(entries, result.entries); return entries.length < result.count ? fetchMoreEntries() : entries; @@ -633,7 +759,8 @@ export class AddonModGlossaryProvider { const promises = []; if (!onlyEntriesList) { - promises.push(this.fetchAllEntries(this.getEntriesByLetter, [glossary.id, 'ALL'], true, siteId).then((entries) => { + promises.push(this.fetchAllEntries(this.getEntriesByLetter, [glossary.id, 'ALL'], true, false, siteId) + .then((entries) => { return this.invalidateEntries(entries, siteId); })); } @@ -644,7 +771,7 @@ export class AddonModGlossaryProvider { promises.push(this.invalidateEntriesByLetter(glossary.id, 'ALL', siteId)); break; case 'cat': - promises.push(this.invalidateEntriesByCategory(glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATERGORIES, + promises.push(this.invalidateEntriesByCategory(glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATEGORIES, siteId)); break; case 'date': @@ -850,7 +977,7 @@ export class AddonModGlossaryProvider { // If we get here, there's no offline entry with this name, check online. // Get entries from the cache. - return this.fetchAllEntries(this.getEntriesByLetter, [glossaryId, 'ALL'], true, siteId).then((entries) => { + return this.fetchAllEntries(this.getEntriesByLetter, [glossaryId, 'ALL'], true, false, siteId).then((entries) => { // Check if there's any entry with the same concept. return entries.some((entry) => entry.concept == concept); }); @@ -906,4 +1033,44 @@ export class AddonModGlossaryProvider { return this.logHelper.logSingle('mod_glossary_view_entry', params, AddonModGlossaryProvider.COMPONENT, glossaryId, name, 'glossary', {entryid: entryId}, siteId); } + + /** + * Store several entries so we can determine their glossaryId in offline. + * + * @param {number} glossaryId Glossary ID the entries belongs to. + * @param {any[]} entries Entries. + * @param {number} from The "page" the entries belong to. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + protected storeEntries(glossaryId: number, entries: any[], from: number, siteId?: string): Promise { + const promises = []; + + entries.forEach((entry) => { + promises.push(this.storeEntryId(glossaryId, entry.id, from, siteId)); + }); + + return Promise.all(promises); + } + + /** + * Store an entry so we can determine its glossaryId in offline. + * + * @param {number} glossaryId Glossary ID the entry belongs to. + * @param {number} entryId Entry ID. + * @param {number} from The "page" the entry belongs to. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + protected storeEntryId(glossaryId: number, entryId: number, from: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const entry = { + entryid: entryId, + glossaryid: glossaryId, + pagefrom: from + }; + + return site.getDb().insertRecord(AddonModGlossaryProvider.ENTRIES_TABLE, entry); + }); + } } diff --git a/src/addon/mod/glossary/providers/prefetch-handler.ts b/src/addon/mod/glossary/providers/prefetch-handler.ts index ba5be62f39c..47369498e6d 100644 --- a/src/addon/mod/glossary/providers/prefetch-handler.ts +++ b/src/addon/mod/glossary/providers/prefetch-handler.ts @@ -139,17 +139,17 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH break; case 'cat': // Not implemented. promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByCategory, - [glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATERGORIES], false, siteId)); + [glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATEGORIES], false, false, siteId)); break; case 'date': promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByDate, - [glossary.id, 'CREATION', 'DESC'], false, siteId)); + [glossary.id, 'CREATION', 'DESC'], false, false, siteId)); promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByDate, - [glossary.id, 'UPDATE', 'DESC'], false, siteId)); + [glossary.id, 'UPDATE', 'DESC'], false, false, siteId)); break; case 'author': promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByAuthor, - [glossary.id, 'ALL', 'LASTNAME', 'ASC'], false, siteId)); + [glossary.id, 'ALL', 'LASTNAME', 'ASC'], false, false, siteId)); break; default: } @@ -157,13 +157,12 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH // Fetch all entries to get information from. promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByLetter, - [glossary.id, 'ALL'], false, siteId).then((entries) => { + [glossary.id, 'ALL'], false, false, siteId).then((entries) => { const promises = []; const avatars = {}; // List of user avatars, preventing duplicates. entries.forEach((entry) => { - // Fetch individual entries. - promises.push(this.glossaryProvider.getEntry(entry.id, siteId)); + // Don't fetch individual entries, it's too many WS calls. if (entry.userpictureurl) { avatars[entry.userpictureurl] = true; @@ -180,6 +179,10 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH return Promise.all(promises); })); + // Prefetch data for link handlers. + promises.push(this.courseProvider.getModuleBasicInfo(module.id, siteId)); + promises.push(this.courseProvider.getModuleBasicInfoByInstance(glossary.id, 'glossary', siteId)); + return Promise.all(promises); }); } diff --git a/src/classes/site.ts b/src/classes/site.ts index 9253f83e631..37ae0616ab2 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -60,6 +60,12 @@ export interface CoreSiteWSPreSets { */ emergencyCache?: boolean; + /** + * If true, the app won't call the WS. If the data isn't cached, the call will fail. + * @type {boolean} + */ + forceOffline?: boolean; + /** * Extra key to add to the cache when storing this call, to identify the entry. * @type {string} @@ -668,7 +674,12 @@ export class CoreSite { } const promise = this.getFromCache(method, data, preSets, false, originalData).catch(() => { - // Do not pass those options to the core WS factory. + if (preSets.forceOffline) { + // Don't call the WS, just fail. + return Promise.reject(this.wsProvider.createFakeWSError('core.cannotconnect', true)); + } + + // Call the WS. return this.callOrEnqueueRequest(method, data, preSets, wsPreSets).then((response) => { if (preSets.saveToCache) { this.saveToCache(method, data, response, preSets); @@ -1043,7 +1054,7 @@ export class CoreSite { const now = Date.now(); let expirationTime; - preSets.omitExpires = preSets.omitExpires || !this.appProvider.isOnline(); + preSets.omitExpires = preSets.omitExpires || preSets.forceOffline || !this.appProvider.isOnline(); if (!preSets.omitExpires) { let expirationDelay = this.UPDATE_FREQUENCIES[preSets.updateFrequency] || From 007804494d838166f7be2fae2c114e4e201416c0 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 22 Jul 2019 15:52:47 +0200 Subject: [PATCH 108/241] MOBILE-3061 dashboard: Pass download enabled as input --- .../components/myoverview/myoverview.ts | 28 ++++++++----------- .../recentlyaccessedcourses.ts | 28 ++++++++----------- .../addon-block-sitemainmenu.html | 10 ++++--- .../components/sitemainmenu/sitemainmenu.ts | 26 +++++++++-------- .../starredcourses/starredcourses.ts | 28 ++++++++----------- src/core/block/components/block/block.ts | 25 ++++++++++++++--- .../courses/pages/dashboard/dashboard.html | 2 +- .../components/index/core-sitehome-index.html | 2 +- src/core/sitehome/components/index/index.ts | 5 ++-- 9 files changed, 81 insertions(+), 73 deletions(-) diff --git a/src/addon/block/myoverview/components/myoverview/myoverview.ts b/src/addon/block/myoverview/components/myoverview/myoverview.ts index b36202f7e09..d257764c2da 100644 --- a/src/addon/block/myoverview/components/myoverview/myoverview.ts +++ b/src/addon/block/myoverview/components/myoverview/myoverview.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, Input, OnDestroy, ViewChild, Injector } from '@angular/core'; +import { Component, OnInit, Input, OnDestroy, ViewChild, Injector, OnChanges, SimpleChange } from '@angular/core'; import { Searchbar } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -32,7 +32,7 @@ import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component selector: 'addon-block-myoverview', templateUrl: 'addon-block-myoverview.html' }) -export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implements OnInit, OnDestroy { +export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy { @ViewChild('searchbar') searchbar: Searchbar; @Input() downloadEnabled: boolean; @@ -67,7 +67,6 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem protected prefetchIconsInitialized = false; protected isDestroyed; - protected downloadButtonObserver; protected coursesObserver; protected updateSiteObserver; protected courseIds = []; @@ -87,18 +86,6 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem */ ngOnInit(): void { // Refresh the enabled flags if enabled. - this.downloadButtonObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, - (data) => { - const wasEnabled = this.downloadEnabled; - - this.downloadEnabled = data.enabled; - - if (!wasEnabled && this.downloadEnabled && this.loaded) { - // Download all courses is enabled now, initialize it. - this.initPrefetchCoursesIcons(); - } - }); - this.downloadCourseEnabled = !this.coursesProvider.isDownloadCourseDisabledInSite(); this.downloadCoursesEnabled = !this.coursesProvider.isDownloadCoursesDisabledInSite(); @@ -128,6 +115,16 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem }); } + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) { + // Download all courses is enabled now, initialize it. + this.initPrefetchCoursesIcons(); + } + } + /** * Perform the invalidate content function. * @@ -350,6 +347,5 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem this.isDestroyed = true; this.coursesObserver && this.coursesObserver.off(); this.updateSiteObserver && this.updateSiteObserver.off(); - this.downloadButtonObserver && this.downloadButtonObserver.off(); } } diff --git a/src/addon/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts b/src/addon/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts index 2d962d17cfc..92697e4f0b7 100644 --- a/src/addon/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts +++ b/src/addon/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, OnDestroy, Injector, Input } from '@angular/core'; +import { Component, OnInit, OnDestroy, Injector, Input, OnChanges, SimpleChange } from '@angular/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSitesProvider } from '@providers/sites'; @@ -30,7 +30,7 @@ import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component selector: 'addon-block-recentlyaccessedcourses', templateUrl: 'addon-block-recentlyaccessedcourses.html' }) -export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnDestroy { +export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy { @Input() downloadEnabled: boolean; courses = []; @@ -41,7 +41,6 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom protected prefetchIconsInitialized = false; protected isDestroyed; - protected downloadButtonObserver; protected coursesObserver; protected courseIds = []; protected fetchContentDefaultError = 'Error getting recent courses data.'; @@ -59,18 +58,6 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom * Component being initialized. */ ngOnInit(): void { - // Refresh the enabled flags if enabled. - this.downloadButtonObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, - (data) => { - const wasEnabled = this.downloadEnabled; - - this.downloadEnabled = data.enabled; - - if (!wasEnabled && this.downloadEnabled && this.loaded) { - // Download all courses is enabled now, initialize it. - this.initPrefetchCoursesIcons(); - } - }); this.coursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, () => { this.refreshContent(); @@ -79,6 +66,16 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom super.ngOnInit(); } + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) { + // Download all courses is enabled now, initialize it. + this.initPrefetchCoursesIcons(); + } + } + /** * Perform the invalidate content function. * @@ -155,6 +152,5 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom ngOnDestroy(): void { this.isDestroyed = true; this.coursesObserver && this.coursesObserver.off(); - this.downloadButtonObserver && this.downloadButtonObserver.off(); } } diff --git a/src/addon/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html b/src/addon/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html index e1a3c10a527..f9a5b7f825d 100644 --- a/src/addon/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html +++ b/src/addon/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html @@ -2,9 +2,11 @@

{{ 'addon.block_sitemainmenu.pluginname' | translate }}

- - - + + + + - + + diff --git a/src/addon/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts b/src/addon/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts index 0f335521904..23bfb74ca8f 100644 --- a/src/addon/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts +++ b/src/addon/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, Injector } from '@angular/core'; +import { Component, OnInit, Injector, Input } from '@angular/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; @@ -28,7 +28,9 @@ import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component templateUrl: 'addon-block-sitemainmenu.html' }) export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent implements OnInit { - block: any; + @Input() downloadEnabled: boolean; + + mainMenuBlock: any; siteHomeId: number; protected fetchContentDefaultError = 'Error getting main menu data.'; @@ -60,9 +62,9 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl promises.push(this.courseProvider.invalidateSections(this.siteHomeId)); promises.push(this.siteHomeProvider.invalidateNewsForum(this.siteHomeId)); - if (this.block && this.block.modules) { + if (this.mainMenuBlock && this.mainMenuBlock.modules) { // Invalidate modules prefetch data. - promises.push(this.prefetchDelegate.invalidateModules(this.block.modules, this.siteHomeId)); + promises.push(this.prefetchDelegate.invalidateModules(this.mainMenuBlock.modules, this.siteHomeId)); } return Promise.all(promises); @@ -75,11 +77,11 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl */ protected fetchContent(): Promise { return this.courseProvider.getSections(this.siteHomeId, false, true).then((sections) => { - this.block = sections.find((section) => section.section == 0); + this.mainMenuBlock = sections.find((section) => section.section == 0); - if (this.block) { - this.block.hasContent = this.courseHelper.sectionHasContent(this.block); - this.courseHelper.addHandlerDataForModules([this.block], this.siteHomeId); + if (this.mainMenuBlock) { + this.mainMenuBlock.hasContent = this.courseHelper.sectionHasContent(this.mainMenuBlock); + this.courseHelper.addHandlerDataForModules([this.mainMenuBlock], this.siteHomeId); // Check if Site Home displays announcements. If so, remove it from the main menu block. const currentSite = this.sitesProvider.getCurrentSite(), @@ -92,15 +94,15 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl hasNewsItem = items.find((item) => { return item == '0'; }); } - if (hasNewsItem && this.block.modules) { + if (hasNewsItem && this.mainMenuBlock.modules) { // Remove forum activity (news one only) from the main menu block to prevent duplicates. return this.siteHomeProvider.getNewsForum(this.siteHomeId).then((forum) => { // Search the module that belongs to site news. - for (let i = 0; i < this.block.modules.length; i++) { - const module = this.block.modules[i]; + for (let i = 0; i < this.mainMenuBlock.modules.length; i++) { + const module = this.mainMenuBlock.modules[i]; if (module.modname == 'forum' && module.instance == forum.id) { - this.block.modules.splice(i, 1); + this.mainMenuBlock.modules.splice(i, 1); break; } } diff --git a/src/addon/block/starredcourses/components/starredcourses/starredcourses.ts b/src/addon/block/starredcourses/components/starredcourses/starredcourses.ts index ffa235134ec..ca25f3354be 100644 --- a/src/addon/block/starredcourses/components/starredcourses/starredcourses.ts +++ b/src/addon/block/starredcourses/components/starredcourses/starredcourses.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, OnDestroy, Injector, Input } from '@angular/core'; +import { Component, OnInit, OnDestroy, Injector, Input, OnChanges, SimpleChange } from '@angular/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSitesProvider } from '@providers/sites'; @@ -30,7 +30,7 @@ import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component selector: 'addon-block-starredcourses', templateUrl: 'addon-block-starredcourses.html' }) -export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnDestroy { +export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy { @Input() downloadEnabled: boolean; courses = []; @@ -41,7 +41,6 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im protected prefetchIconsInitialized = false; protected isDestroyed; - protected downloadButtonObserver; protected coursesObserver; protected courseIds = []; protected fetchContentDefaultError = 'Error getting starred courses data.'; @@ -59,18 +58,6 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im * Component being initialized. */ ngOnInit(): void { - // Refresh the enabled flags if enabled. - this.downloadButtonObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, - (data) => { - const wasEnabled = this.downloadEnabled; - - this.downloadEnabled = data.enabled; - - if (!wasEnabled && this.downloadEnabled && this.loaded) { - // Download all courses is enabled now, initialize it. - this.initPrefetchCoursesIcons(); - } - }); this.coursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, () => { this.refreshContent(); @@ -79,6 +66,16 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im super.ngOnInit(); } + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) { + // Download all courses is enabled now, initialize it. + this.initPrefetchCoursesIcons(); + } + } + /** * Perform the invalidate content function. * @@ -155,6 +152,5 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im ngOnDestroy(): void { this.isDestroyed = true; this.coursesObserver && this.coursesObserver.off(); - this.downloadButtonObserver && this.downloadButtonObserver.off(); } } diff --git a/src/core/block/components/block/block.ts b/src/core/block/components/block/block.ts index faf8aa3be89..eebf4fd4d08 100644 --- a/src/core/block/components/block/block.ts +++ b/src/core/block/components/block/block.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnInit, Injector, ViewChild, OnDestroy } from '@angular/core'; +import { Component, Input, OnInit, Injector, ViewChild, OnDestroy, DoCheck, KeyValueDiffers } from '@angular/core'; import { CoreBlockDelegate } from '../../providers/delegate'; import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; import { Subscription } from 'rxjs'; @@ -25,7 +25,7 @@ import { CoreEventsProvider } from '@providers/events'; selector: 'core-block', templateUrl: 'core-block.html' }) -export class CoreBlockComponent implements OnInit, OnDestroy { +export class CoreBlockComponent implements OnInit, OnDestroy, DoCheck { @ViewChild(CoreDynamicComponent) dynamicComponent: CoreDynamicComponent; @Input() block: any; // The block to render. @@ -40,8 +40,12 @@ export class CoreBlockComponent implements OnInit, OnDestroy { blockSubscription: Subscription; - constructor(protected injector: Injector, protected blockDelegate: CoreBlockDelegate, - protected eventsProvider: CoreEventsProvider) { } + protected differ: any; // To detect changes in the data input. + + constructor(protected injector: Injector, protected blockDelegate: CoreBlockDelegate, differs: KeyValueDiffers, + protected eventsProvider: CoreEventsProvider) { + this.differ = differs.find([]).create(); + } /** * Component being initialized. @@ -57,6 +61,19 @@ export class CoreBlockComponent implements OnInit, OnDestroy { this.initBlock(); } + /** + * Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays). + */ + ngDoCheck(): void { + if (this.data) { + // Check if there's any change in the extraData object. + const changes = this.differ.diff(this.extraData); + if (changes) { + this.data = Object.assign(this.data, this.extraData || {}); + } + } + } + /** * Get block display data and initialises the block once this is available. If the block is not * supported at the moment, try again if the available blocks are updated (because it comes diff --git a/src/core/courses/pages/dashboard/dashboard.html b/src/core/courses/pages/dashboard/dashboard.html index f9eff8228fe..d94fcdd0a88 100644 --- a/src/core/courses/pages/dashboard/dashboard.html +++ b/src/core/courses/pages/dashboard/dashboard.html @@ -26,7 +26,7 @@ - + diff --git a/src/core/sitehome/components/index/core-sitehome-index.html b/src/core/sitehome/components/index/core-sitehome-index.html index a8a1ed1bb86..17a52f8b012 100644 --- a/src/core/sitehome/components/index/core-sitehome-index.html +++ b/src/core/sitehome/components/index/core-sitehome-index.html @@ -24,7 +24,7 @@ - + diff --git a/src/core/sitehome/components/index/index.ts b/src/core/sitehome/components/index/index.ts index 34aa44057a6..a629f76c46b 100644 --- a/src/core/sitehome/components/index/index.ts +++ b/src/core/sitehome/components/index/index.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, ViewChildren, QueryList } from '@angular/core'; +import { Component, OnInit, ViewChildren, QueryList, Input } from '@angular/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -31,6 +31,7 @@ import { CoreSite } from '@classes/site'; }) export class CoreSiteHomeIndexComponent implements OnInit { @ViewChildren(CoreBlockComponent) blocksComponents: QueryList; + @Input() downloadEnabled: boolean; dataLoaded = false; section: any; @@ -40,7 +41,6 @@ export class CoreSiteHomeIndexComponent implements OnInit { siteHomeId: number; currentSite: CoreSite; blocks = []; - downloadEnabled: boolean; constructor(private domUtils: CoreDomUtilsProvider, sitesProvider: CoreSitesProvider, private courseProvider: CoreCourseProvider, private courseHelper: CoreCourseHelperProvider, @@ -53,7 +53,6 @@ export class CoreSiteHomeIndexComponent implements OnInit { * Component being initialized. */ ngOnInit(): void { - this.downloadEnabled = !this.currentSite.isOfflineDisabled(); this.loadContent().finally(() => { this.dataLoaded = true; }); From d20e2e057ef03286fcbff0e0d04a5d58b4b85643 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 22 Jul 2019 14:07:22 +0200 Subject: [PATCH 109/241] MOBILE-3086 data: Handle links in templates --- src/addon/mod/data/components/index/index.ts | 6 ++-- src/addon/mod/data/pages/edit/edit.ts | 2 +- src/addon/mod/data/pages/entry/entry.ts | 2 +- src/addon/mod/data/pages/search/search.ts | 2 +- src/addon/mod/data/providers/helper.ts | 35 ++++++++++++++++---- 5 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts index 37a806f4321..69d6fdcbfe1 100644 --- a/src/addon/mod/data/components/index/index.ts +++ b/src/addon/mod/data/components/index/index.ts @@ -299,14 +299,14 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp if (!this.isEmpty) { this.entries = entries.offlineEntries.concat(entries.entries); - let entriesHTML = this.data.listtemplateheader || ''; + let entriesHTML = this.dataHelper.getTemplate(this.data, 'listtemplateheader', this.fieldsArray); // Get first entry from the whole list. if (!this.search.searching || !this.firstEntry) { this.firstEntry = this.entries[0].id; } - const template = this.data.listtemplate || this.dataHelper.getDefaultTemplate('list', this.fieldsArray); + const template = this.dataHelper.getTemplate(this.data, 'listtemplate', this.fieldsArray); const entriesById = {}; this.entries.forEach((entry, index) => { @@ -318,7 +318,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp entriesHTML += this.dataHelper.displayShowFields(template, this.fieldsArray, entry, offset, 'list', actions); }); - entriesHTML += this.data.listtemplatefooter || ''; + entriesHTML += this.dataHelper.getTemplate(this.data, 'listtemplatefooter', this.fieldsArray); this.entriesRendered = entriesHTML; diff --git a/src/addon/mod/data/pages/edit/edit.ts b/src/addon/mod/data/pages/edit/edit.ts index 35e74cd068f..52025cfa38f 100644 --- a/src/addon/mod/data/pages/edit/edit.ts +++ b/src/addon/mod/data/pages/edit/edit.ts @@ -287,7 +287,7 @@ export class AddonModDataEditPage { let replace, render, - template = this.data.addtemplate || this.dataHelper.getDefaultTemplate('add', this.fieldsArray); + template = this.dataHelper.getTemplate(this.data, 'addtemplate', this.fieldsArray); // Replace the fields found on template. this.fieldsArray.forEach((field) => { diff --git a/src/addon/mod/data/pages/entry/entry.ts b/src/addon/mod/data/pages/entry/entry.ts index 51e95ce9ce2..b8c7da5ddf9 100644 --- a/src/addon/mod/data/pages/entry/entry.ts +++ b/src/addon/mod/data/pages/entry/entry.ts @@ -163,7 +163,7 @@ export class AddonModDataEntryPage implements OnDestroy { }).then(() => { const actions = this.dataHelper.getActions(this.data, this.access, this.entry); - const template = this.data.singletemplate || this.dataHelper.getDefaultTemplate('single', this.fieldsArray); + const template = this.dataHelper.getTemplate(this.data, 'singletemplate', this.fieldsArray); this.entryHtml = this.dataHelper.displayShowFields(template, this.fieldsArray, this.entry, this.offset, 'show', actions); this.showComments = actions.comments; diff --git a/src/addon/mod/data/pages/search/search.ts b/src/addon/mod/data/pages/search/search.ts index 4ca20b5d04e..036f517b73d 100644 --- a/src/addon/mod/data/pages/search/search.ts +++ b/src/addon/mod/data/pages/search/search.ts @@ -89,7 +89,7 @@ export class AddonModDataSearchPage { search: this.search.advanced }; - let template = this.data.asearchtemplate || this.dataHelper.getDefaultTemplate('asearch', this.fieldsArray), + let template = this.dataHelper.getTemplate(this.data, 'asearchtemplate', this.fieldsArray), replace, render; // Replace the fields found on template. diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts index b5aaadcec53..e6827554565 100644 --- a/src/addon/mod/data/providers/helper.ts +++ b/src/addon/mod/data/providers/helper.ts @@ -410,10 +410,14 @@ export class AddonModDataHelperProvider { * @param {any[]} fields List of database fields. * @return {string} Template HTML. */ - getDefaultTemplate( type: 'add' | 'list' | 'single' | 'asearch', fields: any[]): string { + getDefaultTemplate(type: string, fields: any[]): string { + if (type == 'listtemplateheader' || type == 'listtemplatefooter') { + return ''; + } + const html = []; - if (type == 'list') { + if (type == 'listtemplate') { html.push('##delcheck##
'); } @@ -432,7 +436,7 @@ export class AddonModDataHelperProvider { ); }); - if (type == 'list') { + if (type == 'listtemplate') { html.push( '', '', @@ -440,7 +444,7 @@ export class AddonModDataHelperProvider { '', '' ); - } else if (type == 'single') { + } else if (type == 'singletemplate') { html.push( '', '', @@ -448,7 +452,7 @@ export class AddonModDataHelperProvider { '', '' ); - } else if (type == 'asearch') { + } else if (type == 'asearchtemplate') { html.push( '', 'Author first name: ', @@ -467,7 +471,7 @@ export class AddonModDataHelperProvider { '

' ); - if (type == 'list') { + if (type == 'listtemplate') { html.push('
'); } @@ -583,6 +587,25 @@ export class AddonModDataHelperProvider { }); } + /** + * Returns the template of a certain type. + * + * @param {any} data Database object. + * @param {string} type Type of template. + * @param {any[]} fields List of database fields. + * @return {string} Template HTML. + */ + getTemplate(data: any, type: string, fields: any[]): string { + let template = data[type] || this.getDefaultTemplate(type, fields); + + // Add core-link directive to links. + template = template.replace(/]*href="[^>]*)>/i, (match, attributes) => { + return ''; + }); + + return template; + } + /** * Check if data has been changed by the user. * From 3788afe8b343c1622b8dfd01f12b666b8d8d6821 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 22 Jul 2019 14:08:33 +0200 Subject: [PATCH 110/241] MOBILE-3086 data: Fix syntax errors in templates --- src/addon/mod/data/providers/helper.ts | 3 +++ src/providers/utils/dom.ts | 27 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts index e6827554565..a425a5cc6e3 100644 --- a/src/addon/mod/data/providers/helper.ts +++ b/src/addon/mod/data/providers/helper.ts @@ -598,6 +598,9 @@ export class AddonModDataHelperProvider { getTemplate(data: any, type: string, fields: any[]): string { let template = data[type] || this.getDefaultTemplate(type, fields); + // Try to fix syntax errors so the template can be parsed by Angular. + template = this.domUtils.fixHtml(template); + // Add core-link directive to links. template = template.replace(/]*href="[^>]*)>/i, (match, attributes) => { return ''; diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index 24374f3cd2a..4a9ad7ed683 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -323,6 +323,33 @@ export class CoreDomUtilsProvider { return urls; } + /** + * Fix syntax errors in HTML. + * + * @param {string} html HTML text. + * @return {string} Fixed HTML text. + */ + fixHtml(html: string): string { + this.template.innerHTML = html; + + const attrNameRegExp = /[^\x00-\x20\x7F-\x9F"'>\/=]+/; + + const fixElement = (element: Element): void => { + // Remove attributes with an invalid name. + Array.from(element.attributes).forEach((attr) => { + if (!attrNameRegExp.test(attr.name)) { + element.removeAttributeNode(attr); + } + }); + + Array.from(element.children).forEach(fixElement); + }; + + Array.from(this.template.content.children).forEach(fixElement); + + return this.template.innerHTML; + } + /** * Focus an element and open keyboard. * From ab3c15d9b7ccdf06a37156a1d50af859d5958f48 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 22 Jul 2019 15:49:11 +0200 Subject: [PATCH 111/241] MOBILE-3089 android: Allow non-secure HTTP --- config.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config.xml b/config.xml index 211280ed847..744ea653e08 100644 --- a/config.xml +++ b/config.xml @@ -147,6 +147,9 @@ + + + From 1024f6a96dd152483bdedeb0eea01577550a0620 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 24 Jul 2019 09:00:12 +0200 Subject: [PATCH 112/241] MOBILE-3055 courses: Trigger event if user courses list changes --- src/core/courses/providers/courses.ts | 56 +++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/src/core/courses/providers/courses.ts b/src/core/courses/providers/courses.ts index c50cc965674..c63780c5cb9 100644 --- a/src/core/courses/providers/courses.ts +++ b/src/core/courses/providers/courses.ts @@ -13,6 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreSite } from '@classes/site'; @@ -24,13 +25,16 @@ import { CoreSite } from '@classes/site'; export class CoreCoursesProvider { static SEARCH_PER_PAGE = 20; static ENROL_INVALID_KEY = 'CoreCoursesEnrolInvalidKey'; - static EVENT_MY_COURSES_UPDATED = 'courses_my_courses_updated'; + static EVENT_MY_COURSES_CHANGED = 'courses_my_courses_changed'; // User course list changed while app is running. + static EVENT_MY_COURSES_UPDATED = 'courses_my_courses_updated'; // A course was hidden/favourite, or user enroled in a course. static EVENT_MY_COURSES_REFRESHED = 'courses_my_courses_refreshed'; static EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED = 'dashboard_download_enabled_changed'; + protected ROOT_CACHE_KEY = 'mmCourses:'; protected logger; + protected userCoursesIds: {[id: number]: boolean}; // Use an object to make it faster to search. - constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider) { + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private eventsProvider: CoreEventsProvider) { this.logger = logger.getInstance('CoreCoursesProvider'); } @@ -743,7 +747,53 @@ export class CoreCoursesProvider { data.returnusercount = 0; } - return site.read('core_enrol_get_users_courses', data, preSets); + return site.read('core_enrol_get_users_courses', data, preSets).then((courses) => { + if (this.userCoursesIds) { + // Check if the list of courses has changed. + const added = [], + removed = [], + previousIds = Object.keys(this.userCoursesIds), + currentIds = {}; // Use an object to make it faster to search. + + courses.forEach((course) => { + currentIds[course.id] = true; + + if (!this.userCoursesIds[course.id]) { + // Course added. + added.push(course.id); + } + }); + + if (courses.length - added.length != previousIds.length) { + // A course was removed, check which one. + previousIds.forEach((id) => { + if (!currentIds[id]) { + // Course removed. + removed.push(Number(id)); + } + }); + } + + if (added.length || removed.length) { + // At least 1 course was added or removed, trigger the event. + this.eventsProvider.trigger(CoreCoursesProvider.EVENT_MY_COURSES_CHANGED, { + added: added, + removed: removed + }, site.getId()); + } + + this.userCoursesIds = currentIds; + } else { + this.userCoursesIds = {}; + + // Store the list of courses. + courses.forEach((course) => { + this.userCoursesIds[course.id] = true; + }); + } + + return courses; + }); }); } From e3944a9e72355c526b959bbb1e05108f995c1608 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 24 Jul 2019 11:53:38 +0200 Subject: [PATCH 113/241] MOBILE-3055 core: Update site plugins init if course added --- src/core/course/providers/options-delegate.ts | 16 ++-- .../classes/handlers/course-option-handler.ts | 34 +++++++- .../classes/handlers/user-handler.ts | 25 +++++- src/core/siteplugins/providers/helper.ts | 86 +++++++++++++++++-- src/providers/events.ts | 1 + 5 files changed, 139 insertions(+), 23 deletions(-) diff --git a/src/core/course/providers/options-delegate.ts b/src/core/course/providers/options-delegate.ts index b99cc776005..8f0ac612415 100644 --- a/src/core/course/providers/options-delegate.ts +++ b/src/core/course/providers/options-delegate.ts @@ -552,16 +552,12 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { /** * Update handlers for each course. - * - * @param {string} [siteId] Site ID. - */ - updateData(siteId?: string): void { - if (this.sitesProvider.getCurrentSiteId() === siteId) { - // Update handlers for all courses. - for (const courseId in this.coursesHandlers) { - const handler = this.coursesHandlers[courseId]; - this.updateHandlersForCourse(parseInt(courseId, 10), handler.access, handler.navOptions, handler.admOptions); - } + */ + updateData(): void { + // Update handlers for all courses. + for (const courseId in this.coursesHandlers) { + const handler = this.coursesHandlers[courseId]; + this.updateHandlersForCourse(parseInt(courseId, 10), handler.access, handler.navOptions, handler.admOptions); } } diff --git a/src/core/siteplugins/classes/handlers/course-option-handler.ts b/src/core/siteplugins/classes/handlers/course-option-handler.ts index e918ecf115c..e3fde2bd2ee 100644 --- a/src/core/siteplugins/classes/handlers/course-option-handler.ts +++ b/src/core/siteplugins/classes/handlers/course-option-handler.ts @@ -19,6 +19,7 @@ import { } from '@core/course/providers/options-delegate'; import { CoreSitePluginsBaseHandler } from './base-handler'; import { CoreSitePluginsCourseOptionComponent } from '../../components/course-option/course-option'; +import { CoreUtilsProvider, PromiseDefer } from '@providers/utils/utils'; /** * Handler to display a site plugin in course options. @@ -27,8 +28,11 @@ export class CoreSitePluginsCourseOptionHandler extends CoreSitePluginsBaseHandl priority: number; isMenuHandler: boolean; + protected updatingDefer: PromiseDefer; + constructor(name: string, protected title: string, protected plugin: any, protected handlerSchema: any, - protected initResult: any, protected sitePluginsProvider: CoreSitePluginsProvider) { + protected initResult: any, protected sitePluginsProvider: CoreSitePluginsProvider, + protected utils: CoreUtilsProvider) { super(name); this.priority = handlerSchema.priority; @@ -45,8 +49,13 @@ export class CoreSitePluginsCourseOptionHandler extends CoreSitePluginsBaseHandl * @return {boolean|Promise} True or promise resolved with true if enabled. */ isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise { - return this.sitePluginsProvider.isHandlerEnabledForCourse( - courseId, this.handlerSchema.restricttoenrolledcourses, this.initResult.restrict); + // Wait for "init" result to be updated. + const promise = this.updatingDefer ? this.updatingDefer.promise : Promise.resolve(); + + return promise.then(() => { + return this.sitePluginsProvider.isHandlerEnabledForCourse( + courseId, this.handlerSchema.restricttoenrolledcourses, this.initResult.restrict); + }); } /** @@ -108,4 +117,23 @@ export class CoreSitePluginsCourseOptionHandler extends CoreSitePluginsBaseHandl return this.sitePluginsProvider.prefetchFunctions(component, args, this.handlerSchema, course.id, undefined, true); } + + /** + * Set init result. + * + * @param {any} result Result to set. + */ + setInitResult(result: any): void { + this.initResult = result; + + this.updatingDefer.resolve(); + delete this.updatingDefer; + } + + /** + * Mark init being updated. + */ + updatingInit(): void { + this.updatingDefer = this.utils.promiseDefer(); + } } diff --git a/src/core/siteplugins/classes/handlers/user-handler.ts b/src/core/siteplugins/classes/handlers/user-handler.ts index 044fd231765..5bd01b063de 100644 --- a/src/core/siteplugins/classes/handlers/user-handler.ts +++ b/src/core/siteplugins/classes/handlers/user-handler.ts @@ -16,6 +16,7 @@ import { NavController } from 'ionic-angular'; import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate'; import { CoreSitePluginsProvider } from '../../providers/siteplugins'; import { CoreSitePluginsBaseHandler } from './base-handler'; +import { CoreUtilsProvider, PromiseDefer } from '@providers/utils/utils'; /** * Handler to display a site plugin in the user profile. @@ -37,8 +38,11 @@ export class CoreSitePluginsUserProfileHandler extends CoreSitePluginsBaseHandle */ type: string; + protected updatingDefer: PromiseDefer; + constructor(name: string, protected title: string, protected plugin: any, protected handlerSchema: any, - protected initResult: any, protected sitePluginsProvider: CoreSitePluginsProvider) { + protected initResult: any, protected sitePluginsProvider: CoreSitePluginsProvider, + protected utils: CoreUtilsProvider) { super(name); this.priority = handlerSchema.priority; @@ -97,4 +101,23 @@ export class CoreSitePluginsUserProfileHandler extends CoreSitePluginsBaseHandle } }; } + + /** + * Set init result. + * + * @param {any} result Result to set. + */ + setInitResult(result: any): void { + this.initResult = result; + + this.updatingDefer.resolve(); + delete this.updatingDefer; + } + + /** + * Mark init being updated. + */ + updatingInit(): void { + this.updatingDefer = this.utils.promiseDefer(); + } } diff --git a/src/core/siteplugins/providers/helper.ts b/src/core/siteplugins/providers/helper.ts index 2b1242d85aa..ded88ec7309 100644 --- a/src/core/siteplugins/providers/helper.ts +++ b/src/core/siteplugins/providers/helper.ts @@ -30,6 +30,7 @@ import { CoreSitePluginsProvider } from './siteplugins'; import { CoreCompileProvider } from '@core/compile/providers/compile'; import { CoreQuestionProvider } from '@core/question/providers/question'; import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; // Delegates import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; @@ -79,13 +80,14 @@ import { CoreSitePluginsBlockHandler } from '@core/siteplugins/classes/handlers/ @Injectable() export class CoreSitePluginsHelperProvider { protected logger; + protected courseRestrictHandlers = {}; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider, private mainMenuDelegate: CoreMainMenuDelegate, private moduleDelegate: CoreCourseModuleDelegate, private userDelegate: CoreUserDelegate, private langProvider: CoreLangProvider, private http: Http, private sitePluginsProvider: CoreSitePluginsProvider, private prefetchDelegate: CoreCourseModulePrefetchDelegate, private compileProvider: CoreCompileProvider, private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider, - private courseOptionsDelegate: CoreCourseOptionsDelegate, eventsProvider: CoreEventsProvider, + private courseOptionsDelegate: CoreCourseOptionsDelegate, private eventsProvider: CoreEventsProvider, private courseFormatDelegate: CoreCourseFormatDelegate, private profileFieldDelegate: CoreUserProfileFieldDelegate, private textUtils: CoreTextUtilsProvider, private filepoolProvider: CoreFilepoolProvider, private settingsDelegate: CoreSettingsDelegate, private questionDelegate: CoreQuestionDelegate, @@ -122,6 +124,13 @@ export class CoreSitePluginsHelperProvider { window.location.reload(); } }); + + // Re-load plugins restricted for courses when the list of user courses changes. + eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_CHANGED, (data) => { + if (data && data.siteId && data.siteId == this.sitesProvider.getCurrentSiteId() && data.added && data.added.length) { + this.reloadCourseRestrictHandlers(); + } + }); } /** @@ -375,6 +384,7 @@ export class CoreSitePluginsHelperProvider { */ loadSitePlugins(plugins: any[]): Promise { const promises = []; + this.courseRestrictHandlers = {}; plugins.forEach((plugin) => { const pluginPromise = this.loadSitePlugin(plugin); @@ -683,10 +693,21 @@ export class CoreSitePluginsHelperProvider { // Create and register the handler. const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), - prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title); - - this.courseOptionsDelegate.registerHandler(new CoreSitePluginsCourseOptionHandler(uniqueName, prefixedTitle, plugin, - handlerSchema, initResult, this.sitePluginsProvider)); + prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title), + handler = new CoreSitePluginsCourseOptionHandler(uniqueName, prefixedTitle, plugin, + handlerSchema, initResult, this.sitePluginsProvider, this.utils); + + this.courseOptionsDelegate.registerHandler(handler); + + if (initResult && initResult.restrict && initResult.restrict.courses) { + // This handler is restricted to certan courses, store it in the list. + this.courseRestrictHandlers[uniqueName] = { + plugin: plugin, + handlerName: handlerName, + handlerSchema: handlerSchema, + handler: handler + }; + } return uniqueName; } @@ -889,10 +910,21 @@ export class CoreSitePluginsHelperProvider { // Create and register the handler. const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), - prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title); - - this.userDelegate.registerHandler(new CoreSitePluginsUserProfileHandler(uniqueName, prefixedTitle, plugin, handlerSchema, - initResult, this.sitePluginsProvider)); + prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title), + handler = new CoreSitePluginsUserProfileHandler(uniqueName, prefixedTitle, plugin, handlerSchema, + initResult, this.sitePluginsProvider, this.utils); + + this.userDelegate.registerHandler(handler); + + if (initResult && initResult.restrict && initResult.restrict.courses) { + // This handler is restricted to certan courses, store it in the list. + this.courseRestrictHandlers[uniqueName] = { + plugin: plugin, + handlerName: handlerName, + handlerSchema: handlerSchema, + handler: handler + }; + } return uniqueName; } @@ -935,4 +967,40 @@ export class CoreSitePluginsHelperProvider { return new CoreSitePluginsWorkshopAssessmentStrategyHandler(uniqueName, strategyName); }); } + + /** + * Reload the handlers that are restricted to certain courses. + * + * @return {Promise} Promise resolved when done. + */ + protected reloadCourseRestrictHandlers(): Promise { + if (!Object.keys(this.courseRestrictHandlers).length) { + // No course restrict handlers, nothing to do. + return Promise.resolve(); + } + + const promises = []; + + for (const name in this.courseRestrictHandlers) { + const data = this.courseRestrictHandlers[name]; + + if (!data.handler || !data.handler.setInitResult) { + // No handler or it doesn't implement a required function, ignore it. + continue; + } + + // Mark the handler as being updated. + data.handler.updatingInit && data.handler.updatingInit(); + + promises.push(this.executeHandlerInit(data.plugin, data.handlerSchema).then((initResult) => { + data.handler.setInitResult(initResult); + }).catch((error) => { + this.logger.error('Error reloading course restrict handler', error, data.plugin); + })); + } + + return Promise.all(promises).then(() => { + this.eventsProvider.trigger(CoreEventsProvider.SITE_PLUGINS_COURSE_RESTRICT_UPDATED, {}); + }); + } } diff --git a/src/providers/events.ts b/src/providers/events.ts index fe75a177a43..48f3dcf322c 100644 --- a/src/providers/events.ts +++ b/src/providers/events.ts @@ -48,6 +48,7 @@ export class CoreEventsProvider { static COURSE_STATUS_CHANGED = 'course_status_changed'; static SECTION_STATUS_CHANGED = 'section_status_changed'; static SITE_PLUGINS_LOADED = 'site_plugins_loaded'; + static SITE_PLUGINS_COURSE_RESTRICT_UPDATED = 'site_plugins_course_restrict_updated'; static LOGIN_SITE_CHECKED = 'login_site_checked'; static LOGIN_SITE_UNCHECKED = 'login_site_unchecked'; static IAB_LOAD_START = 'inappbrowser_load_start'; From 8dd01089070148a60eda4621e20be61740004341 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 4 Jul 2019 13:16:54 +0200 Subject: [PATCH 114/241] MOBILE-3021 calendar: Support upcoming events --- scripts/langindex.json | 6 + .../calendar/addon-calendar-calendar.html | 3 +- .../calendar/components/calendar/calendar.ts | 7 + .../calendar/components/components.module.ts | 11 +- .../addon-calendar-upcoming-events.html | 24 ++ .../upcoming-events/upcoming-events.ts | 317 ++++++++++++++++++ src/addon/calendar/lang/en.json | 8 +- src/addon/calendar/pages/index/index.html | 10 +- src/addon/calendar/pages/index/index.ts | 21 +- src/addon/calendar/providers/calendar.ts | 207 +++++++++++- src/app/app.scss | 5 + src/assets/lang/en.json | 6 + 12 files changed, 613 insertions(+), 12 deletions(-) create mode 100644 src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html create mode 100644 src/addon/calendar/components/upcoming-events/upcoming-events.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 2f79cd0a08b..1d2d82d13f9 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -83,6 +83,7 @@ "addon.blog.publishtoworld": "blog", "addon.blog.showonlyyourentries": "local_moodlemobileapp", "addon.blog.siteblogheading": "blog", + "addon.calendar.allday": "calendar", "addon.calendar.calendar": "calendar", "addon.calendar.calendarevent": "local_moodlemobileapp", "addon.calendar.calendarevents": "local_moodlemobileapp", @@ -113,6 +114,7 @@ "addon.calendar.invalidtimedurationuntil": "calendar", "addon.calendar.mon": "calendar", "addon.calendar.monday": "calendar", + "addon.calendar.monthlyview": "calendar", "addon.calendar.newevent": "calendar", "addon.calendar.noevents": "local_moodlemobileapp", "addon.calendar.nopermissiontoupdatecalendar": "error", @@ -129,6 +131,8 @@ "addon.calendar.sunday": "calendar", "addon.calendar.thu": "calendar", "addon.calendar.thursday": "calendar", + "addon.calendar.today": "calendar", + "addon.calendar.tomorrow": "calendar", "addon.calendar.tue": "calendar", "addon.calendar.tuesday": "calendar", "addon.calendar.typecategory": "calendar", @@ -140,8 +144,10 @@ "addon.calendar.typeopen": "calendar", "addon.calendar.typesite": "calendar", "addon.calendar.typeuser": "calendar", + "addon.calendar.upcomingevents": "calendar", "addon.calendar.wed": "calendar", "addon.calendar.wednesday": "calendar", + "addon.calendar.yesterday": "calendar", "addon.competency.activities": "tool_lp", "addon.competency.competencies": "competency", "addon.competency.competenciesmostoftennotproficientincourse": "tool_lp", diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index a866c4be2fb..007100baf60 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -37,12 +37,13 @@

-
+

+ {{ event.timestart * 1000 | coreFormatDate: timeFormat }} {{event.name}}

diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 05386348389..203db79e8f1 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -42,6 +42,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest weekDays: any[]; weeks: any[]; loaded = false; + timeFormat: string; protected year: number; protected month: number; @@ -141,6 +142,11 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.deletedEvents = ids; })); + // Get time format to use. + promises.push(this.calendarProvider.getCalendarTimeFormat().then((value) => { + this.timeFormat = value; + })); + return Promise.all(promises).then(() => { return this.fetchEvents(); }).catch((error) => { @@ -232,6 +238,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest promises.push(this.calendarProvider.invalidateMonthlyEvents(this.year, this.month)); promises.push(this.coursesProvider.invalidateCategories(0, true)); + promises.push(this.calendarProvider.invalidateTimeFormat()); this.categoriesRetrieved = false; // Get categories again. diff --git a/src/addon/calendar/components/components.module.ts b/src/addon/calendar/components/components.module.ts index a6d5afe2254..3928f6dd914 100644 --- a/src/addon/calendar/components/components.module.ts +++ b/src/addon/calendar/components/components.module.ts @@ -18,23 +18,28 @@ import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; import { AddonCalendarCalendarComponent } from '../components/calendar/calendar'; +import { AddonCalendarUpcomingEventsComponent } from '../components/upcoming-events/upcoming-events'; @NgModule({ declarations: [ - AddonCalendarCalendarComponent + AddonCalendarCalendarComponent, + AddonCalendarUpcomingEventsComponent ], imports: [ CommonModule, IonicModule, TranslateModule.forChild(), CoreComponentsModule, - CoreDirectivesModule + CoreDirectivesModule, + CorePipesModule ], providers: [ ], exports: [ - AddonCalendarCalendarComponent + AddonCalendarCalendarComponent, + AddonCalendarUpcomingEventsComponent ] }) export class AddonCalendarComponentsModule {} diff --git a/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html b/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html new file mode 100644 index 00000000000..b338f0ecaf1 --- /dev/null +++ b/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html @@ -0,0 +1,24 @@ + + + + + + +
+ + +

+

+ + + {{ 'core.notsent' | translate }} + + + + {{ 'core.deletedoffline' | translate }} + +
+ + + + diff --git a/src/addon/calendar/components/upcoming-events/upcoming-events.ts b/src/addon/calendar/components/upcoming-events/upcoming-events.ts new file mode 100644 index 00000000000..41751c0d5f5 --- /dev/null +++ b/src/addon/calendar/components/upcoming-events/upcoming-events.ts @@ -0,0 +1,317 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, OnDestroy, OnInit, Input, OnChanges, SimpleChange, Output, EventEmitter } from '@angular/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreConstants } from '@core/constants'; + +/** + * Component that displays upcoming events. + */ +@Component({ + selector: 'addon-calendar-upcoming-events', + templateUrl: 'addon-calendar-upcoming-events.html', +}) +export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, OnDestroy { + @Input() courseId: number | string; + @Input() categoryId: number | string; // Category ID the course belongs to. + @Output() onEventClicked = new EventEmitter(); + + events = []; // Events (both online and offline). + filteredEvents = []; + loaded = false; + + protected year: number; + protected month: number; + protected categoriesRetrieved = false; + protected categories = {}; + protected currentSiteId: string; + protected onlineEvents = []; + protected offlineEvents = []; // Offline events. + protected deletedEvents = []; // Events deleted in offline. + protected lookAhead: number; + protected timeFormat: string; + + // Observers. + protected undeleteEventObserver: any; + + constructor(eventsProvider: CoreEventsProvider, + sitesProvider: CoreSitesProvider, + private calendarProvider: AddonCalendarProvider, + private calendarHelper: AddonCalendarHelperProvider, + private calendarOffline: AddonCalendarOfflineProvider, + private domUtils: CoreDomUtilsProvider, + private coursesProvider: CoreCoursesProvider) { + + this.currentSiteId = sitesProvider.getCurrentSiteId(); + + // Listen for events "undeleted" (offline). + this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => { + if (data && data.eventId) { + // Mark it as undeleted, no need to refresh. + this.undeleteEvent(data.eventId); + + // Remove it from the list of deleted events if it's there. + const index = this.deletedEvents.indexOf(data.eventId); + if (index != -1) { + this.deletedEvents.splice(index, 1); + } + } + }, this.currentSiteId); + } + + /** + * Component loaded. + */ + ngOnInit(): void { + this.fetchData(); + } + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + if (changes.courseId || changes.categoryId) { + this.filterEvents(); + } + } + + /** + * Fetch data. + * + * @param {boolean} [refresh=false] True if we are refreshing events. + * @return {Promise} Promise resolved when done. + */ + fetchData(refresh: boolean = false): Promise { + const promises = []; + + promises.push(this.loadCategories()); + + // Get offline events. + promises.push(this.calendarOffline.getAllEditedEvents().then((events) => { + // Format data. + events.forEach((event) => { + event.offline = true; + this.calendarHelper.formatEventData(event); + }); + + this.offlineEvents = this.sortEvents(events); + })); + + // Get events deleted in offline. + promises.push(this.calendarOffline.getAllDeletedEventsIds().then((ids) => { + this.deletedEvents = ids; + })); + + // Get user preferences. + promises.push(this.calendarProvider.getCalendarLookAhead().then((value) => { + this.lookAhead = value; + })); + + promises.push(this.calendarProvider.getCalendarTimeFormat().then((value) => { + this.timeFormat = value; + })); + + return Promise.all(promises).then(() => { + return this.fetchEvents(); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Fetch upcoming events. + * + * @return {Promise} Promise resolved when done. + */ + fetchEvents(): Promise { + // Don't pass courseId and categoryId, we'll filter them locally. + return this.calendarProvider.getUpcomingEvents().then((result) => { + this.onlineEvents = result.events; + + this.onlineEvents.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + + // Merge the online events with offline data. + this.events = this.mergeEvents(); + + // Filter events by course. + this.filterEvents(); + + // Re-calculate the formatted time so it uses the device date. + this.events.forEach((event) => { + event.formattedtime = this.calendarProvider.formatEventTime(event, this.timeFormat); + }); + }); + } + + /** + * Load categories to be able to filter events. + * + * @return {Promise} Promise resolved when done. + */ + protected loadCategories(): Promise { + if (this.categoriesRetrieved) { + // Already retrieved, stop. + return Promise.resolve(); + } + + return this.coursesProvider.getCategories(0, true).then((cats) => { + this.categoriesRetrieved = true; + this.categories = {}; + + // Index categories by ID. + cats.forEach((category) => { + this.categories[category.id] = category; + }); + }).catch(() => { + // Ignore errors. + }); + } + + /** + * Filter events to only display events belonging to a certain course. + */ + filterEvents(): void { + const courseId = this.courseId ? Number(this.courseId) : undefined, + categoryId = this.categoryId ? Number(this.categoryId) : undefined; + + if (!courseId || courseId < 0) { + this.filteredEvents = this.events; + } else { + this.filteredEvents = this.events.filter((event) => { + return this.calendarHelper.shouldDisplayEvent(event, courseId, categoryId, this.categories); + }); + } + } + + /** + * Refresh events. + * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + refreshData(sync?: boolean, showErrors?: boolean): Promise { + const promises = []; + + promises.push(this.calendarProvider.invalidateAllUpcomingEvents()); + promises.push(this.coursesProvider.invalidateCategories(0, true)); + promises.push(this.calendarProvider.invalidateLookAhead()); + promises.push(this.calendarProvider.invalidateTimeFormat()); + + this.categoriesRetrieved = false; // Get categories again. + + return Promise.all(promises).then(() => { + return this.fetchData(true); + }); + } + + /** + * An event was clicked. + * + * @param {any} event Event. + */ + eventClicked(event: any): void { + this.onEventClicked.emit(event.id); + } + + /** + * Merge online events with the offline events of that period. + * + * @return {any[]} Merged events. + */ + protected mergeEvents(): any[] { + if (!this.offlineEvents.length && !this.deletedEvents.length) { + // No offline events, nothing to merge. + return this.onlineEvents; + } + + const start = Date.now(), + end = start + (CoreConstants.SECONDS_DAY * this.lookAhead); + let result = this.onlineEvents; + + if (this.deletedEvents.length) { + // Mark as deleted the events that were deleted in offline. + result.forEach((event) => { + event.deleted = this.deletedEvents.indexOf(event.id) != -1; + }); + } + + if (this.offlineEvents.length) { + // Remove the online events that were modified in offline. + result = result.filter((event) => { + const offlineEvent = this.offlineEvents.find((ev) => { + return ev.id == event.id; + }); + + return !offlineEvent; + }); + } + + // Now get the offline events that belong to this period. + const periodOfflineEvents = this.offlineEvents.filter((event) => { + return (event.timestart >= start || event.timestart + event.timeduration >= start) && event.timestart <= end; + }); + + // Merge both arrays and sort them. + result = result.concat(periodOfflineEvents); + + return this.sortEvents(result); + } + + /** + * Sort events by timestart. + * + * @param {any[]} events List to sort. + */ + protected sortEvents(events: any[]): any[] { + return events.sort((a, b) => { + if (a.timestart == b.timestart) { + return a.timeduration - b.timeduration; + } + + return a.timestart - b.timestart; + }); + } + + /** + * Undelete a certain event. + * + * @param {number} eventId Event ID. + */ + protected undeleteEvent(eventId: number): void { + const event = this.onlineEvents.find((event) => { + return event.id == eventId; + }); + + if (event) { + event.deleted = false; + } + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.undeleteEventObserver && this.undeleteEventObserver.off(); + } +} diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index 412c8174240..ba81a9a89a7 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -1,4 +1,5 @@ { + "allday": "All day", "calendar": "Calendar", "calendarevent": "Calendar event", "calendarevents": "Calendar events", @@ -29,6 +30,7 @@ "invalidtimedurationuntil": "The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.", "mon": "Mon", "monday": "Monday", + "monthlyview": "Monthly view", "newevent": "New event", "noevents": "There are no events", "nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event", @@ -45,6 +47,8 @@ "sunday": "Sunday", "thu": "Thu", "thursday": "Thursday", + "today": "Today", + "tomorrow": "Tomorrow", "tue": "Tue", "tuesday": "Tuesday", "typeclose": "Close event", @@ -56,6 +60,8 @@ "typeopen": "Open event", "typesite": "Site event", "typeuser": "User event", + "upcomingevents": "Upcoming events", "wed": "Wed", - "wednesday": "Wednesday" + "wednesday": "Wednesday", + "yesterday": "Yesterday" } \ No newline at end of file diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html index fa0877b46da..120c9dc2c30 100644 --- a/src/addon/calendar/pages/index/index.html +++ b/src/addon/calendar/pages/index/index.html @@ -2,6 +2,12 @@ {{ 'addon.calendar.calendarevents' | translate }} + + @@ -22,7 +28,9 @@ {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }} - + + + diff --git a/src/addon/calendar/pages/index/index.ts b/src/addon/calendar/pages/index/index.ts index 8f134ed904c..3966904278c 100644 --- a/src/addon/calendar/pages/index/index.ts +++ b/src/addon/calendar/pages/index/index.ts @@ -23,6 +23,7 @@ import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarCalendarComponent } from '../../components/calendar/calendar'; +import { AddonCalendarUpcomingEventsComponent } from '../../components/upcoming-events/upcoming-events'; import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; @@ -39,6 +40,7 @@ import { Network } from '@ionic-native/network'; }) export class AddonCalendarIndexPage implements OnInit, OnDestroy { @ViewChild(AddonCalendarCalendarComponent) calendarComponent: AddonCalendarCalendarComponent; + @ViewChild(AddonCalendarUpcomingEventsComponent) upcomingEventsComponent: AddonCalendarUpcomingEventsComponent; protected allCourses = { id: -1, @@ -67,6 +69,8 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { hasOffline = false; isOnline = false; syncIcon: string; + showCalendar = true; + loadUpcoming = false; constructor(localNotificationsProvider: CoreLocalNotificationsProvider, navParams: NavParams, @@ -274,7 +278,11 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { })); // Refresh the sub-component. - promises.push(this.calendarComponent.refreshData()); + if (this.showCalendar && this.calendarComponent) { + promises.push(this.calendarComponent.refreshData()); + } else if (!this.showCalendar && this.upcomingEventsComponent) { + promises.push(this.upcomingEventsComponent.refreshData()); + } return Promise.all(promises).finally(() => { return this.fetchData(sync, showErrors); @@ -349,6 +357,17 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { this.navCtrl.push('AddonCalendarSettingsPage'); } + /** + * Toogle display: monthly view or upcoming events. + */ + toggleDisplay(): void { + this.showCalendar = !this.showCalendar; + + if (!this.showCalendar) { + this.loadUpcoming = true; + } + } + /** * Page destroyed. */ diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 4705a8ad80b..c759210db3a 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -27,7 +27,9 @@ import { CoreConfigProvider } from '@providers/config'; import { ILocalNotification } from '@ionic-native/local-notifications'; import { SQLiteDB } from '@classes/sqlitedb'; import { AddonCalendarOfflineProvider } from './calendar-offline'; +import { CoreUserProvider } from '@core/user/providers/user'; import { TranslateService } from '@ngx-translate/core'; +import * as moment from 'moment'; /** * Service to handle calendar events. @@ -49,6 +51,10 @@ export class AddonCalendarProvider { static TYPE_GROUP = 'group'; static TYPE_SITE = 'site'; static TYPE_USER = 'user'; + + static CALENDAR_TF_24 = '%H:%M'; // Calendar time in 24 hours format. + static CALENDAR_TF_12 = '%I:%M %p'; // Calendar time in 12 hours format. + protected ROOT_CACHE_KEY = 'mmaCalendar:'; protected weekDays = [ @@ -249,11 +255,19 @@ export class AddonCalendarProvider { protected logger; - constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider, - private coursesProvider: CoreCoursesProvider, private timeUtils: CoreTimeUtilsProvider, - private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider, - private utils: CoreUtilsProvider, private calendarOffline: AddonCalendarOfflineProvider, - private appProvider: CoreAppProvider, private translate: TranslateService) { + constructor(logger: CoreLoggerProvider, + private sitesProvider: CoreSitesProvider, + private groupsProvider: CoreGroupsProvider, + private coursesProvider: CoreCoursesProvider, + private timeUtils: CoreTimeUtilsProvider, + private localNotificationsProvider: CoreLocalNotificationsProvider, + private configProvider: CoreConfigProvider, + private utils: CoreUtilsProvider, + private calendarOffline: AddonCalendarOfflineProvider, + private appProvider: CoreAppProvider, + private translate: TranslateService, + private userProvider: CoreUserProvider) { + this.logger = logger.getInstance('AddonCalendarProvider'); this.sitesProvider.registerSiteSchema(this.siteSchema); } @@ -456,6 +470,90 @@ export class AddonCalendarProvider { }); } + /** + * Check if event ends the same day or not. + * + * @param {any} event Event info. + * @return {boolean} If the . + */ + endsSameDay(event: any): boolean { + if (!event.timeduration) { + // No duration. + return true; + } + + // Check if day has changed. + return moment(event.timestart * 1000).isSame((event.timestart + event.timeduration) * 1000, 'day'); + } + + /** + * Format event time. Equivalent to calendar_format_event_time. + * + * @param {any} event Event to format. + * @param {string} format Calendar time format (from getCalendarTimeFormat). + * @param {boolean} [useCommonWords=true] Whether to use common words like "Today", "Yesterday", etc. + * @param {number} [showTime=0] Determine the show time GMT timestamp. + * @return {string} Formatted event time. + */ + formatEventTime(event: any, format: string, useCommonWords: boolean = true, showTime: number = 0): string { + const start = event.timestart * 1000, + end = (event.timestart + event.timeduration) * 1000; + let time; + + if (event.timeduration) { + + if (moment(start).isSame(end, 'day')) { + // Event starts and ends the same day. + if (event.timeduration == CoreConstants.SECONDS_DAY) { + time = this.translate.instant('addon.calendar.allday'); + } else { + time = this.timeUtils.userDate(start, format) + ' » ' + + this.timeUtils.userDate(end, format); + } + + if (!showTime) { + return this.getDayRepresentation(start, useCommonWords) + ', ' + time; + } else { + return time; + } + + } else { + // Event lasts more than one day. + const midnightStart = moment(start).startOf('day').unix() * 1000, + midnightEnd = moment(end).startOf('day').unix() * 1000, + timeStart = this.timeUtils.userDate(start, format), + timeEnd = this.timeUtils.userDate(end, format); + let dayStart = this.getDayRepresentation(start, useCommonWords) + ', ', + dayEnd = this.getDayRepresentation(end, useCommonWords) + ', '; + + if (showTime == midnightStart) { + dayStart = ''; + } + + if (showTime == midnightEnd) { + dayEnd = ''; + } + + // Set printable representation. + if (moment().isSame(start, 'day')) { + // Event starts today, don't display the day. + return timeStart + ' » ' + dayEnd + timeEnd; + } else { + // The event starts in the future, print both days. + return dayStart + timeStart + ' » ' + dayEnd + timeEnd; + } + } + } else { // There is no time duration. + const time = this.timeUtils.userDate(start, format); + + if (!showTime) { + return this.getDayRepresentation(start, useCommonWords) + ', ' + time.trim(); + } else { + return time; + } + } + } + /** * Get access information for a calendar (either course calendar or site calendar). * @@ -545,6 +643,84 @@ export class AddonCalendarProvider { return this.ROOT_CACHE_KEY + 'allowedEventTypes:' + (courseId || 0); } + /** + * Get the "look ahead" for a certain user. + * + * @param {string} [siteId] ID of the site. If not defined, use current site. + * @return {Promise} Promise resolved with the look ahead (number of days). + */ + getCalendarLookAhead(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.userProvider.getUserPreference('calendar_lookahead').catch((error) => { + // Ignore errors. + }).then((value): any => { + if (typeof value != 'undefined') { + return value; + } + + return site.getStoredConfig('calendar_lookahead'); + }); + }); + } + + /** + * Get the time format to use in calendar. + * + * @param {string} [siteId] ID of the site. If not defined, use current site. + * @return {Promise} Promise resolved with the format. + */ + getCalendarTimeFormat(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.userProvider.getUserPreference('calendar_timeformat').catch((error) => { + // Ignore errors. + }).then((format) => { + + if (!format || format === '0') { + format = site.getStoredConfig('calendar_site_timeformat'); + } + + if (format === AddonCalendarProvider.CALENDAR_TF_12) { + format = this.translate.instant('core.strftimetime12'); + } else if (format === AddonCalendarProvider.CALENDAR_TF_24) { + format = this.translate.instant('core.strftimetime24'); + } + + return format && format !== '0' ? format : this.translate.instant('core.strftimetime'); + }); + }); + } + + /** + * Return the representation day. Equivalent to Moodle's calendar_day_representation. + * + * @param {number} time Timestamp to get the day from. + * @param {boolean} [useCommonWords=true] Whether to use common words like "Today", "Yesterday", etc. + * @return {string} The formatted date/time. + */ + getDayRepresentation(time: number, useCommonWords: boolean = true): string { + + if (!useCommonWords) { + // We don't want words, just a date. + return this.timeUtils.userDate(time, 'core.strftimedayshort'); + } + + const date = moment(time), + today = moment(); + + if (date.isSame(today, 'day')) { + return this.translate.instant('addon.calendar.today'); + + } else if (date.isSame(today.clone().subtract(1, 'days'), 'day')) { + return this.translate.instant('addon.calendar.yesterday'); + + } else if (date.isSame(today.clone().add(1, 'days'), 'day')) { + return this.translate.instant('addon.calendar.tomorrow'); + + } else { + return this.timeUtils.userDate(time, 'core.strftimedayshort'); + } + } + /** * Get the configured default notification time. * @@ -1019,6 +1195,7 @@ export class AddonCalendarProvider { * * @param {number} [courseId] Course ID. * @param {number} [categoryId] Category ID. + * @param {string} [siteId] Site Id. If not defined, use current site. * @return {Promise} Promise resolved when the data is invalidated. */ invalidateUpcomingEvents(courseId?: number, categoryId?: number, siteId?: string): Promise { @@ -1027,6 +1204,26 @@ export class AddonCalendarProvider { }); } + /** + * Invalidates look ahead setting. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateLookAhead(siteId?: string): Promise { + return this.userProvider.invalidateUserPreference('calendar_lookahead', siteId); + } + + /** + * Invalidates time format setting. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateTimeFormat(siteId?: string): Promise { + return this.userProvider.invalidateUserPreference('calendar_timeformat', siteId); + } + /** * Check if Calendar is disabled in a certain site. * diff --git a/src/app/app.scss b/src/app/app.scss index 41759e8eff1..7123b9909da 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -1152,3 +1152,8 @@ ion-app.platform-desktop { min-height: $button-ios-small-height; } } + +// Make funnel icon have iOS look. +.ion-md-funnel::before { + content: "\f182"; +} diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 3e4a0a98405..7dae77a7099 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -83,6 +83,7 @@ "addon.blog.publishtoworld": "Anyone in the world", "addon.blog.showonlyyourentries": "Show only your entries", "addon.blog.siteblogheading": "Site blog", + "addon.calendar.allday": "All day", "addon.calendar.calendar": "Calendar", "addon.calendar.calendarevent": "Calendar event", "addon.calendar.calendarevents": "Calendar events", @@ -113,6 +114,7 @@ "addon.calendar.invalidtimedurationuntil": "The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.", "addon.calendar.mon": "Mon", "addon.calendar.monday": "Monday", + "addon.calendar.monthlyview": "Monthly view", "addon.calendar.newevent": "New event", "addon.calendar.noevents": "There are no events", "addon.calendar.nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event", @@ -129,6 +131,8 @@ "addon.calendar.sunday": "Sunday", "addon.calendar.thu": "Thu", "addon.calendar.thursday": "Thursday", + "addon.calendar.today": "Today", + "addon.calendar.tomorrow": "Tomorrow", "addon.calendar.tue": "Tue", "addon.calendar.tuesday": "Tuesday", "addon.calendar.typecategory": "Category event", @@ -140,8 +144,10 @@ "addon.calendar.typeopen": "Open event", "addon.calendar.typesite": "Site event", "addon.calendar.typeuser": "User event", + "addon.calendar.upcomingevents": "Upcoming events", "addon.calendar.wed": "Wed", "addon.calendar.wednesday": "Wednesday", + "addon.calendar.yesterday": "Yesterday", "addon.competency.activities": "Activities", "addon.competency.competencies": "Competencies", "addon.competency.competenciesmostoftennotproficientincourse": "Competencies most often not proficient in this course", From 083ca7cd7ee9b3228443dbe32f71d649820389a8 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 5 Jul 2019 10:30:38 +0200 Subject: [PATCH 115/241] MOBILE-3021 calendar: Implement day view --- scripts/langindex.json | 2 + .../calendar/addon-calendar-calendar.html | 4 +- .../components/calendar/calendar.scss | 2 +- .../calendar/components/calendar/calendar.ts | 10 + .../upcoming-events/upcoming-events.ts | 2 +- src/addon/calendar/lang/en.json | 2 + src/addon/calendar/pages/day/day.html | 71 ++ src/addon/calendar/pages/day/day.module.ts | 35 + src/addon/calendar/pages/day/day.ts | 607 ++++++++++++++++++ .../calendar/pages/edit-event/edit-event.ts | 4 +- src/addon/calendar/pages/event/event.ts | 1 - src/addon/calendar/pages/index/index.html | 2 +- src/addon/calendar/pages/index/index.ts | 72 +-- src/addon/calendar/pages/list/list.ts | 64 +- src/addon/calendar/providers/calendar.ts | 99 +++ src/assets/lang/en.json | 2 + src/core/courses/providers/helper.ts | 74 ++- 17 files changed, 962 insertions(+), 91 deletions(-) create mode 100644 src/addon/calendar/pages/day/day.html create mode 100644 src/addon/calendar/pages/day/day.module.ts create mode 100644 src/addon/calendar/pages/day/day.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 1d2d82d13f9..d80355727c2 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -90,6 +90,8 @@ "addon.calendar.calendarreminders": "local_moodlemobileapp", "addon.calendar.confirmeventdelete": "calendar", "addon.calendar.confirmeventseriesdelete": "calendar", + "addon.calendar.daynext": "calendar", + "addon.calendar.dayprev": "calendar", "addon.calendar.defaultnotificationtime": "local_moodlemobileapp", "addon.calendar.deleteallevents": "calendar", "addon.calendar.deleteevent": "calendar", diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index 007100baf60..7c607ca636f 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -31,7 +31,7 @@ -

{{ day.mday }}

+

{{ day.mday }}

@@ -47,7 +47,7 @@ {{event.name}}

-

{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}

+

{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}

diff --git a/src/addon/calendar/components/calendar/calendar.scss b/src/addon/calendar/components/calendar/calendar.scss index c3a20bf9e81..af7182c321c 100644 --- a/src/addon/calendar/components/calendar/calendar.scss +++ b/src/addon/calendar/components/calendar/calendar.scss @@ -20,7 +20,7 @@ ion-app.app-root addon-calendar-calendar { } } - .addon-calendar-event { + .addon-calendar-event, .addon-calendar-day-number, .addon-calendar-day-more { cursor: pointer; } diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 203db79e8f1..2b40b8b3f0b 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -37,6 +37,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest @Input() categoryId: number | string; // Category ID the course belongs to. @Input() canNavigate?: string | boolean; // Whether to include arrows to change the month. Defaults to true. @Output() onEventClicked = new EventEmitter(); + @Output() onDayClicked = new EventEmitter<{day: number, month: number, year: number}>(); periodName: string; weekDays: any[]; @@ -288,6 +289,15 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.onEventClicked.emit(event.id); } + /** + * A day was clicked. + * + * @param {number} day Day. + */ + dayClicked(day: number): void { + this.onDayClicked.emit({day: day, month: this.month, year: this.year}); + } + /** * Decrease the current month. */ diff --git a/src/addon/calendar/components/upcoming-events/upcoming-events.ts b/src/addon/calendar/components/upcoming-events/upcoming-events.ts index 41751c0d5f5..f358b57857f 100644 --- a/src/addon/calendar/components/upcoming-events/upcoming-events.ts +++ b/src/addon/calendar/components/upcoming-events/upcoming-events.ts @@ -34,7 +34,6 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, @Input() categoryId: number | string; // Category ID the course belongs to. @Output() onEventClicked = new EventEmitter(); - events = []; // Events (both online and offline). filteredEvents = []; loaded = false; @@ -43,6 +42,7 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, protected categoriesRetrieved = false; protected categories = {}; protected currentSiteId: string; + protected events = []; // Events (both online and offline). protected onlineEvents = []; protected offlineEvents = []; // Offline events. protected deletedEvents = []; // Events deleted in offline. diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index ba81a9a89a7..8d19b455941 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -6,6 +6,8 @@ "calendarreminders": "Calendar reminders", "confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?", "confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?", + "daynext": "Next day", + "dayprev": "Previous day", "defaultnotificationtime": "Default notification time", "deleteallevents": "Delete all events", "deleteevent": "Delete event", diff --git a/src/addon/calendar/pages/day/day.html b/src/addon/calendar/pages/day/day.html new file mode 100644 index 00000000000..e9b48bd5261 --- /dev/null +++ b/src/addon/calendar/pages/day/day.html @@ -0,0 +1,71 @@ + + + {{ 'addon.calendar.calendarevents' | translate }} + + + + + + + + + + + + + + + + + + + + + + +

{{ periodName }}

+
+ + + + + +
+
+ + + + {{ 'core.hasdatatosync' | translate:{$a: 'core.day' | translate} }} + + + + + + + + + + +

+

+ + + {{ 'core.notsent' | translate }} + + + + {{ 'core.deletedoffline' | translate }} + +
+
+
+ + + + + +
diff --git a/src/addon/calendar/pages/day/day.module.ts b/src/addon/calendar/pages/day/day.module.ts new file mode 100644 index 00000000000..c4e33dbb9df --- /dev/null +++ b/src/addon/calendar/pages/day/day.module.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonCalendarDayPage } from './day'; + +@NgModule({ + declarations: [ + AddonCalendarDayPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(AddonCalendarDayPage), + TranslateModule.forChild() + ], +}) +export class AddonCalendarDayPageModule {} diff --git a/src/addon/calendar/pages/day/day.ts b/src/addon/calendar/pages/day/day.ts new file mode 100644 index 00000000000..924bfa0e74b --- /dev/null +++ b/src/addon/calendar/pages/day/day.ts @@ -0,0 +1,607 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 OFx ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, OnDestroy, NgZone } from '@angular/core'; +import { IonicPage, NavParams, NavController } from 'ionic-angular'; +import { CoreAppProvider } from '@providers/app'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; +import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreCoursesHelperProvider } from '@core/courses/providers/helper'; +import { Network } from '@ionic-native/network'; +import * as moment from 'moment'; + +/** + * Page that displays the calendar events for a certain day. + */ +@IonicPage({ segment: 'addon-calendar-day' }) +@Component({ + selector: 'page-addon-calendar-day', + templateUrl: 'day.html', +}) +export class AddonCalendarDayPage implements OnInit, OnDestroy { + + protected currentSiteId: string; + protected year: number; + protected month: number; + protected day: number; + protected categories = {}; + protected events = []; // Events (both online and offline). + protected onlineEvents = []; + protected offlineEvents = {}; // Offline events. + protected offlineEditedEventsIds = []; // IDs of events edited in offline. + protected deletedEvents = []; // Events deleted in offline. + protected timeFormat: string; + protected currentMoment: moment.Moment; + + // Observers. + protected newEventObserver: any; + protected discardedObserver: any; + protected editEventObserver: any; + protected deleteEventObserver: any; + protected undeleteEventObserver: any; + protected syncObserver: any; + protected manualSyncObserver: any; + protected onlineObserver: any; + + periodName: string; + filteredEvents = []; + courseId: number; + categoryId: number; + canCreate = false; + courses: any[]; + loaded = false; + hasOffline = false; + isOnline = false; + syncIcon: string; + + constructor(localNotificationsProvider: CoreLocalNotificationsProvider, + navParams: NavParams, + network: Network, + zone: NgZone, + sitesProvider: CoreSitesProvider, + private navCtrl: NavController, + private domUtils: CoreDomUtilsProvider, + private timeUtils: CoreTimeUtilsProvider, + private calendarProvider: AddonCalendarProvider, + private calendarOffline: AddonCalendarOfflineProvider, + private calendarHelper: AddonCalendarHelperProvider, + private calendarSync: AddonCalendarSyncProvider, + private eventsProvider: CoreEventsProvider, + private coursesProvider: CoreCoursesProvider, + private coursesHelper: CoreCoursesHelperProvider, + private appProvider: CoreAppProvider) { + + const now = new Date(); + + this.year = navParams.get('year') || now.getFullYear(); + this.month = navParams.get('month') || (now.getMonth() + 1); + this.day = navParams.get('day') || now.getDate(); + this.courseId = navParams.get('courseId'); + this.currentSiteId = sitesProvider.getCurrentSiteId(); + + // Listen for events added. When an event is added, reload the data. + this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { + if (data && data.event) { + this.loaded = false; + this.refreshData(true, false); + } + }, this.currentSiteId); + + // Listen for new event discarded event. When it does, reload the data. + this.discardedObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => { + this.loaded = false; + this.refreshData(true, false); + }, this.currentSiteId); + + // Listen for events edited. When an event is edited, reload the data. + this.editEventObserver = eventsProvider.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => { + if (data && data.event) { + this.loaded = false; + this.refreshData(true, false); + } + }, this.currentSiteId); + + // Refresh data if calendar events are synchronized automatically. + this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => { + this.loaded = false; + this.refreshData(); + }, this.currentSiteId); + + // Refresh data if calendar events are synchronized manually but not by this page. + this.manualSyncObserver = eventsProvider.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => { + if (data && (data.source != 'day' || data.year != this.year || data.month != this.month || data.day != this.day)) { + this.loaded = false; + this.refreshData(); + } + }, this.currentSiteId); + + // Update the events when an event is deleted. + this.deleteEventObserver = eventsProvider.on(AddonCalendarProvider.DELETED_EVENT_EVENT, (data) => { + if (data && !data.sent) { + // Event was deleted in offline. Just mark it as deleted, no need to refresh. + this.hasOffline = this.markAsDeleted(data.eventId, true) || this.hasOffline; + this.deletedEvents.push(data.eventId); + } else { + this.loaded = false; + this.refreshData(); + } + }, this.currentSiteId); + + // Listen for events "undeleted" (offline). + this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => { + if (data && data.eventId) { + // Mark it as undeleted, no need to refresh. + const found = this.markAsDeleted(data.eventId, false); + + // Remove it from the list of deleted events if it's there. + const index = this.deletedEvents.indexOf(data.eventId); + if (index != -1) { + this.deletedEvents.splice(index, 1); + } + + if (found) { + // The deleted event belongs to current list. Re-calculate "hasOffline". + this.hasOffline = false; + + if (this.events.length != this.onlineEvents.length) { + this.hasOffline = true; + } else { + const event = this.events.find((event) => { + return event.deleted || event.offline; + }); + + this.hasOffline = !!event; + } + } + } + }, this.currentSiteId); + + // Refresh online status when changes. + this.onlineObserver = network.onchange().subscribe(() => { + // Execute the callback in the Angular zone, so change detection doesn't stop working. + zone.run(() => { + this.isOnline = this.appProvider.isOnline(); + }); + }); + } + + /** + * View loaded. + */ + ngOnInit(): void { + this.currentMoment = moment().year(this.year).month(this.month - 1).day(this.day); + + this.fetchData(true, false); + } + + /** + * Fetch all the data required for the view. + * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + fetchData(sync?: boolean, showErrors?: boolean): Promise { + + this.syncIcon = 'spinner'; + this.isOnline = this.appProvider.isOnline(); + + const promise = sync ? this.sync() : Promise.resolve(); + + return promise.then(() => { + const promises = []; + + // Load courses for the popover. + promises.push(this.coursesHelper.getCoursesForPopover(this.courseId).then((data) => { + this.courses = data.courses; + this.categoryId = data.categoryId; + })); + + // Get categories. + promises.push(this.loadCategories()); + + // Get offline events. + promises.push(this.calendarOffline.getAllEditedEvents().then((events) => { + // Format data. + events.forEach((event) => { + event.offline = true; + this.calendarHelper.formatEventData(event); + }); + + // Classify them by month & day. + this.offlineEvents = this.calendarHelper.classifyIntoMonths(events); + + // // Get the IDs of events edited in offline. + const filtered = events.filter((event) => { + return event.id > 0; + }); + this.offlineEditedEventsIds = filtered.map((event) => { + return event.id; + }); + })); + + // Get events deleted in offline. + promises.push(this.calendarOffline.getAllDeletedEventsIds().then((ids) => { + this.deletedEvents = ids; + })); + + // Check if user can create events. + promises.push(this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { + this.canCreate = canEdit; + })); + + // Get user preferences. + promises.push(this.calendarProvider.getCalendarTimeFormat().then((value) => { + this.timeFormat = value; + })); + + return Promise.all(promises); + }).then(() => { + return this.fetchEvents(); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + }).finally(() => { + this.loaded = true; + this.syncIcon = 'sync'; + }); + } + + /** + * Fetch the events for current day. + * + * @return {Promise} Promise resolved when done. + */ + fetchEvents(): Promise { + // Don't pass courseId and categoryId, we'll filter them locally. + return this.calendarProvider.getDayEvents(this.year, this.month, this.day).then((result) => { + + // Calculate the period name. We don't use the one in result because it's in server's language. + this.periodName = this.timeUtils.userDate(new Date(this.year, this.month - 1, this.day).getTime(), + 'core.strftimedaydate'); + + this.onlineEvents = result.events; + this.onlineEvents.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + + // Merge the online events with offline data. + this.events = this.mergeEvents(); + + // Filter events by course. + this.filterEvents(); + + // Re-calculate the formatted time so it uses the device date. + this.events.forEach((event) => { + event.formattedtime = this.calendarProvider.formatEventTime(event, this.timeFormat); + }); + }); + } + + /** + * Merge online events with the offline events of that period. + * + * @return {any[]} Merged events. + */ + protected mergeEvents(): any[] { + this.hasOffline = false; + + if (!this.offlineEditedEventsIds.length && !this.deletedEvents.length) { + // No offline events, nothing to merge. + return this.onlineEvents; + } + + const monthOfflineEvents = this.offlineEvents[this.calendarHelper.getMonthId(this.year, this.month)], + dayOfflineEvents = monthOfflineEvents && monthOfflineEvents[this.day]; + let result = this.onlineEvents; + + if (this.deletedEvents.length) { + // Mark as deleted the events that were deleted in offline. + result.forEach((event) => { + event.deleted = this.deletedEvents.indexOf(event.id) != -1; + + if (event.deleted) { + this.hasOffline = true; + } + }); + } + + if (this.offlineEditedEventsIds.length) { + // Remove the online events that were modified in offline. + result = result.filter((event) => { + return this.offlineEditedEventsIds.indexOf(event.id) == -1; + }); + + if (result.length != this.onlineEvents.length) { + this.hasOffline = true; + } + } + + if (dayOfflineEvents && dayOfflineEvents.length) { + // Add the offline events (either new or edited). + this.hasOffline = true; + result = this.sortEvents(result.concat(dayOfflineEvents)); + } + + return result; + } + + /** + * Filter events to only display events belonging to a certain course. + */ + protected filterEvents(): void { + if (!this.courseId || this.courseId < 0) { + this.filteredEvents = this.events; + } else { + this.filteredEvents = this.events.filter((event) => { + return this.calendarHelper.shouldDisplayEvent(event, this.courseId, this.categoryId, this.categories); + }); + } + } + + /** + * Sort events by timestart. + * + * @param {any[]} events List to sort. + */ + protected sortEvents(events: any[]): any[] { + return events.sort((a, b) => { + if (a.timestart == b.timestart) { + return a.timeduration - b.timeduration; + } + + return a.timestart - b.timestart; + }); + } + + /** + * Refresh the data. + * + * @param {any} [refresher] Refresher. + * @param {Function} [done] Function to call when done. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + doRefresh(refresher?: any, done?: () => void, showErrors?: boolean): Promise { + if (this.loaded) { + return this.refreshData(true, showErrors).finally(() => { + refresher && refresher.complete(); + done && done(); + }); + } + + return Promise.resolve(); + } + + /** + * Refresh the data. + * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + refreshData(sync?: boolean, showErrors?: boolean): Promise { + this.syncIcon = 'spinner'; + + const promises = []; + + promises.push(this.calendarProvider.invalidateAllowedEventTypes()); + promises.push(this.calendarProvider.invalidateDayEvents(this.year, this.month, this.day)); + promises.push(this.coursesProvider.invalidateCategories(0, true)); + promises.push(this.calendarProvider.invalidateTimeFormat()); + + return Promise.all(promises).finally(() => { + return this.fetchData(sync, showErrors); + }); + } + + /** + * Load categories to be able to filter events. + * + * @return {Promise} Promise resolved when done. + */ + protected loadCategories(): Promise { + return this.coursesProvider.getCategories(0, true).then((cats) => { + this.categories = {}; + + // Index categories by ID. + cats.forEach((category) => { + this.categories[category.id] = category; + }); + }).catch(() => { + // Ignore errors. + }); + } + + /** + * Try to synchronize offline events. + * + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + protected sync(showErrors?: boolean): Promise { + return this.calendarSync.syncEvents().then((result) => { + if (result.warnings && result.warnings.length) { + this.domUtils.showErrorModal(result.warnings[0]); + } + + if (result.updated) { + // Trigger a manual sync event. + result.source = 'day'; + result.day = this.day; + result.month = this.month; + result.year = this.year; + + this.eventsProvider.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId); + } + }).catch((error) => { + if (showErrors) { + this.domUtils.showErrorModalDefault(error, 'core.errorsync', true); + } + }); + } + + /** + * Navigate to a particular event. + * + * @param {number} eventId Event to load. + */ + gotoEvent(eventId: number): void { + if (eventId < 0) { + // It's an offline event, go to the edit page. + this.openEdit(eventId); + } else { + this.navCtrl.push('AddonCalendarEventPage', { + id: eventId + }); + } + } + + /** + * Show the context menu. + * + * @param {MouseEvent} event Event. + */ + openCourseFilter(event: MouseEvent): void { + this.coursesHelper.selectCourse(event, this.courses, this.courseId).then((result) => { + if (typeof result.courseId != 'undefined') { + this.courseId = result.courseId > 0 ? result.courseId : undefined; + this.categoryId = result.courseId > 0 ? result.categoryId : undefined; + + // Course viewed has changed, check if the user can create events for this course calendar. + this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { + this.canCreate = canEdit; + }); + + this.filterEvents(); + } + }); + } + + /** + * Open page to create/edit an event. + * + * @param {number} [eventId] Event ID to edit. + */ + openEdit(eventId?: number): void { + const params: any = {}; + + if (eventId) { + params.eventId = eventId; + } else { + // It's a new event, set the time. + params.timestamp = moment().year(this.year).month(this.month - 1).date(this.day).unix() * 1000; + } + + if (this.courseId) { + params.courseId = this.courseId; + } + + this.navCtrl.push('AddonCalendarEditEventPage', params); + } + + /** + * Load next month. + */ + loadNext(): void { + this.increaseDay(); + + this.loaded = false; + + this.fetchEvents().catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + this.decreaseDay(); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Load previous month. + */ + loadPrevious(): void { + this.decreaseDay(); + + this.loaded = false; + + this.fetchEvents().catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + this.increaseDay(); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Decrease the current day. + */ + protected decreaseDay(): void { + this.currentMoment.subtract(1, 'day'); + + this.year = this.currentMoment.year(); + this.month = this.currentMoment.month() + 1; + this.day = this.currentMoment.date(); + } + + /** + * Increase the current day. + */ + protected increaseDay(): void { + this.currentMoment.add(1, 'day'); + + this.year = this.currentMoment.year(); + this.month = this.currentMoment.month() + 1; + this.day = this.currentMoment.date(); + } + + /** + * Find an event and mark it as deleted. + * + * @param {number} eventId Event ID. + * @param {boolean} deleted Whether to mark it as deleted or not. + * @return {boolean} Whether the event was found. + */ + protected markAsDeleted(eventId: number, deleted: boolean): boolean { + const event = this.onlineEvents.find((event) => { + return event.id == eventId; + }); + + if (event) { + event.deleted = deleted; + + return true; + } + + return false; + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.newEventObserver && this.newEventObserver.off(); + this.discardedObserver && this.discardedObserver.off(); + this.editEventObserver && this.editEventObserver.off(); + this.deleteEventObserver && this.deleteEventObserver.off(); + this.undeleteEventObserver && this.undeleteEventObserver.off(); + this.syncObserver && this.syncObserver.off(); + this.manualSyncObserver && this.manualSyncObserver.off(); + this.onlineObserver && this.onlineObserver.unsubscribe(); + } +} diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index edda98102c8..93f8b349906 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -99,6 +99,8 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { this.courseId = navParams.get('courseId'); this.title = this.eventId ? 'addon.calendar.editevent' : 'addon.calendar.newevent'; + const timestamp = navParams.get('timestamp'); + this.currentSite = sitesProvider.getCurrentSite(); this.errors = { required: this.translate.instant('core.required') @@ -114,7 +116,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { this.groupControl = this.fb.control(''); this.descriptionControl = this.fb.control(''); - const currentDate = this.timeUtils.toDatetimeFormat(); + const currentDate = this.timeUtils.toDatetimeFormat(timestamp); this.eventForm.addControl('name', this.fb.control('', Validators.required)); this.eventForm.addControl('timestart', this.fb.control(currentDate, Validators.required)); diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index 9f312a3e807..bfb45499c34 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -493,7 +493,6 @@ export class AddonCalendarEventPage implements OnDestroy { eventId: this.eventId }, this.sitesProvider.getCurrentSiteId()); - this.domUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000, undefined, false); this.event.deleted = false; }).catch((error) => { diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html index 120c9dc2c30..099892df88e 100644 --- a/src/addon/calendar/pages/index/index.html +++ b/src/addon/calendar/pages/index/index.html @@ -28,7 +28,7 @@ {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }} - + diff --git a/src/addon/calendar/pages/index/index.ts b/src/addon/calendar/pages/index/index.ts index 3966904278c..59088156bff 100644 --- a/src/addon/calendar/pages/index/index.ts +++ b/src/addon/calendar/pages/index/index.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, OnInit, OnDestroy, ViewChild, NgZone } from '@angular/core'; -import { IonicPage, NavParams, NavController, PopoverController } from 'ionic-angular'; +import { IonicPage, NavParams, NavController } from 'ionic-angular'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; @@ -25,9 +25,7 @@ import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarCalendarComponent } from '../../components/calendar/calendar'; import { AddonCalendarUpcomingEventsComponent } from '../../components/upcoming-events/upcoming-events'; import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; -import { CoreCoursesProvider } from '@core/courses/providers/courses'; -import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; -import { TranslateService } from '@ngx-translate/core'; +import { CoreCoursesHelperProvider } from '@core/courses/providers/helper'; import { Network } from '@ionic-native/network'; /** @@ -42,11 +40,6 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { @ViewChild(AddonCalendarCalendarComponent) calendarComponent: AddonCalendarCalendarComponent; @ViewChild(AddonCalendarUpcomingEventsComponent) upcomingEventsComponent: AddonCalendarUpcomingEventsComponent; - protected allCourses = { - id: -1, - fullname: this.translate.instant('core.fulllistofcourses'), - category: -1 - }; protected eventId: number; protected currentSiteId: string; @@ -83,10 +76,8 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { private calendarOffline: AddonCalendarOfflineProvider, private calendarHelper: AddonCalendarHelperProvider, private calendarSync: AddonCalendarSyncProvider, - private translate: TranslateService, private eventsProvider: CoreEventsProvider, - private coursesProvider: CoreCoursesProvider, - private popoverCtrl: PopoverController, + private coursesHelper: CoreCoursesHelperProvider, private appProvider: CoreAppProvider) { this.courseId = navParams.get('courseId'); @@ -206,21 +197,9 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { this.hasOffline = false; // Load courses for the popover. - promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { - // Add "All courses". - courses.unshift(this.allCourses); - this.courses = courses; - - if (this.courseId) { - // Search the course to get the category. - const course = this.courses.find((course) => { - return course.id == this.courseId; - }); - - if (course) { - this.categoryId = course.category; - } - } + promises.push(this.coursesHelper.getCoursesForPopover(this.courseId).then((data) => { + this.courses = data.courses; + this.categoryId = data.categoryId; })); // Check if user can create events. @@ -273,9 +252,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { const promises = []; - promises.push(this.calendarProvider.invalidateAllowedEventTypes().then(() => { - return this.fetchData(); - })); + promises.push(this.calendarProvider.invalidateAllowedEventTypes()); // Refresh the sub-component. if (this.showCalendar && this.calendarComponent) { @@ -305,21 +282,35 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { } } + /** + * View a certain day. + * + * @param {any} data Data with the year, month and day. + */ + gotoDay(data: any): void { + const params: any = { + day: data.day, + month: data.month, + year: data.year + }; + + if (this.courseId) { + params.courseId = this.courseId; + } + + this.navCtrl.push('AddonCalendarDayPage', params); + } + /** * Show the context menu. * * @param {MouseEvent} event Event. */ openCourseFilter(event: MouseEvent): void { - const popover = this.popoverCtrl.create(CoreCoursePickerMenuPopoverComponent, { - courses: this.courses, - courseId: this.courseId - }); - - popover.onDidDismiss((course) => { - if (course) { - this.courseId = course.id > 0 ? course.id : undefined; - this.categoryId = course.id > 0 ? course.category : undefined; + this.coursesHelper.selectCourse(event, this.courses, this.courseId).then((result) => { + if (typeof result.courseId != 'undefined') { + this.courseId = result.courseId > 0 ? result.courseId : undefined; + this.categoryId = result.courseId > 0 ? result.categoryId : undefined; // Course viewed has changed, check if the user can create events for this course calendar. this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { @@ -327,9 +318,6 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { }); } }); - popover.present({ - ev: event - }); } /** diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index 00c6657d422..1c7572e1236 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -13,19 +13,18 @@ // limitations under the License. import { Component, ViewChild, OnDestroy, NgZone } from '@angular/core'; -import { IonicPage, Content, PopoverController, NavParams, NavController } from 'ionic-angular'; -import { TranslateService } from '@ngx-translate/core'; +import { IonicPage, Content, NavParams, NavController } from 'ionic-angular'; import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreCoursesHelperProvider } from '@core/courses/providers/helper'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSitesProvider } from '@providers/sites'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; -import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; import { CoreEventsProvider } from '@providers/events'; import { CoreAppProvider } from '@providers/app'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; @@ -50,11 +49,6 @@ export class AddonCalendarListPage implements OnDestroy { protected emptyEventsTimes = 0; // Variable to identify consecutive calls returning 0 events. protected categoriesRetrieved = false; protected getCategories = false; - protected allCourses = { - id: -1, - fullname: this.translate.instant('core.fulllistofcourses'), - category: -1 - }; protected categories = {}; protected siteHomeId: number; protected obsDefaultTimeChange: any; @@ -80,18 +74,17 @@ export class AddonCalendarListPage implements OnDestroy { filteredEvents = []; canLoadMore = false; loadMoreError = false; - filter = { - course: this.allCourses - }; + courseId: number; + categoryId: number; canCreate = false; hasOffline = false; isOnline = false; syncIcon: string; // Sync icon. - constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams, + constructor(private calendarProvider: AddonCalendarProvider, navParams: NavParams, private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, private calendarHelper: AddonCalendarHelperProvider, sitesProvider: CoreSitesProvider, zone: NgZone, - localNotificationsProvider: CoreLocalNotificationsProvider, private popoverCtrl: PopoverController, + localNotificationsProvider: CoreLocalNotificationsProvider, private coursesHelper: CoreCoursesHelperProvider, private eventsProvider: CoreEventsProvider, private navCtrl: NavController, private appProvider: CoreAppProvider, private calendarOffline: AddonCalendarOfflineProvider, private calendarSync: AddonCalendarSyncProvider, network: Network, private timeUtils: CoreTimeUtilsProvider) { @@ -177,6 +170,7 @@ export class AddonCalendarListPage implements OnDestroy { if (data && !data.sent) { // Event was deleted in offline. Just mark it as deleted, no need to refresh. this.markAsDeleted(data.eventId, true); + this.deletedEvents.push(data.eventId); this.hasOffline = true; } else { // Event deleted, clear the details if needed and refresh the view. @@ -278,19 +272,17 @@ export class AddonCalendarListPage implements OnDestroy { return promise.then(() => { const promises = []; - const courseId = this.filter.course.id != this.allCourses.id ? this.filter.course.id : undefined; this.hasOffline = false; - promises.push(this.calendarHelper.canEditEvents(courseId).then((canEdit) => { + promises.push(this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { this.canCreate = canEdit; })); // Load courses for the popover. - promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { - // Add "All courses". - courses.unshift(this.allCourses); - this.courses = courses; + promises.push(this.coursesHelper.getCoursesForPopover(this.courseId).then((result) => { + this.courses = result.courses; + this.categoryId = result.categoryId; if (this.preSelectedCourseId) { this.filter.course = courses.find((course) => { @@ -418,14 +410,13 @@ export class AddonCalendarListPage implements OnDestroy { * @return {any[]} Filtered events. */ protected getFilteredEvents(): any[] { - if (this.filter.course.id == -1) { + if (!this.courseId) { // No filter, display everything. return this.events; } return this.events.filter((event) => { - return this.calendarHelper.shouldDisplayEvent(event, this.filter.course.id, this.filter.course.category, - this.categories); + return this.calendarHelper.shouldDisplayEvent(event, this.courseId, this.categoryId, this.categories); }); } @@ -613,28 +604,21 @@ export class AddonCalendarListPage implements OnDestroy { * @param {MouseEvent} event Event. */ openCourseFilter(event: MouseEvent): void { - const popover = this.popoverCtrl.create(CoreCoursePickerMenuPopoverComponent, { - courses: this.courses, - courseId: this.filter.course.id - }); - popover.onDidDismiss((course) => { - if (course) { - this.filter.course = course; - this.domUtils.scrollToTop(this.content); - - this.filteredEvents = this.getFilteredEvents(); + this.coursesHelper.selectCourse(event, this.courses, this.courseId).then((result) => { + if (typeof result.courseId != 'undefined') { + this.courseId = result.courseId > 0 ? result.courseId : undefined; + this.categoryId = result.courseId > 0 ? result.categoryId : undefined; // Course viewed has changed, check if the user can create events for this course calendar. - const courseId = this.filter.course.id != this.allCourses.id ? this.filter.course.id : undefined; - - this.calendarHelper.canEditEvents(courseId).then((canEdit) => { + this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { this.canCreate = canEdit; }); + + this.filteredEvents = this.getFilteredEvents(); + + this.domUtils.scrollToTop(this.content); } }); - popover.present({ - ev: event - }); } /** @@ -650,8 +634,8 @@ export class AddonCalendarListPage implements OnDestroy { if (eventId) { params.eventId = eventId; } - if (this.filter.course.id != this.allCourses.id) { - params.courseId = this.filter.course.id; + if (this.courseId) { + params.courseId = this.courseId; } this.splitviewCtrl.push('AddonCalendarEditEventPage', params); diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index c759210db3a..3bb8680347c 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -858,6 +858,79 @@ export class AddonCalendarProvider { }); } + /** + * Get calendar events for a certain day. + * + * @param {number} year Year to get. + * @param {number} month Month to get. + * @param {number} day Day to get. + * @param {number} [courseId] Course to get. + * @param {number} [categoryId] Category to get. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the response. + */ + getDayEvents(year: number, month: number, day: number, courseId?: number, categoryId?: number, siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + + const data: any = { + year: year, + month: month, + day: day + }; + + if (courseId) { + data.courseid = courseId; + } + if (categoryId) { + data.categoryid = categoryId; + } + + const preSets = { + cacheKey: this.getDayEventsCacheKey(year, month, day, courseId, categoryId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES + }; + + return site.read('core_calendar_get_calendar_day_view', data, preSets); + }); + } + + /** + * Get prefix cache key for day events WS calls. + * + * @return {string} Prefix Cache key. + */ + protected getDayEventsPrefixCacheKey(): string { + return this.ROOT_CACHE_KEY + 'day:'; + } + + /** + * Get prefix cache key for a certain day for day events WS calls. + * + * @param {number} year Year to get. + * @param {number} month Month to get. + * @param {number} day Day to get. + * @return {string} Prefix Cache key. + */ + protected getDayEventsDayPrefixCacheKey(year: number, month: number, day: number): string { + return this.getDayEventsPrefixCacheKey() + year + ':' + month + ':' + day + ':'; + } + + /** + * Get cache key for day events WS calls. + * + * @param {number} year Year to get. + * @param {number} month Month to get. + * @param {number} day Day to get. + * @param {number} [courseId] Course to get. + * @param {number} [categoryId] Category to get. + * @return {string} Cache key. + */ + protected getDayEventsCacheKey(year: number, month: number, day: number, courseId?: number, categoryId?: number): string { + return this.getDayEventsDayPrefixCacheKey(year, month, day) + (courseId ? courseId : '') + ':' + + (categoryId ? categoryId : ''); + } + /** * Get a calendar reminders from local Db. * @@ -1120,6 +1193,32 @@ export class AddonCalendarProvider { }); } + /** + * Invalidates day events for all days. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAllDayEvents(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getDayEventsPrefixCacheKey()); + }); + } + + /** + * Invalidates day events for a certain day. + * + * @param {number} year Year. + * @param {number} month Month. + * @param {number} day Day. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateDayEvents(year: number, month: number, day: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getDayEventsDayPrefixCacheKey(year, month, day)); + }); + } + /** * Invalidates events list and all the single events and related info. * diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 7dae77a7099..b78160d84c5 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -90,6 +90,8 @@ "addon.calendar.calendarreminders": "Calendar reminders", "addon.calendar.confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?", "addon.calendar.confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?", + "addon.calendar.daynext": "Next day", + "addon.calendar.dayprev": "Previous day", "addon.calendar.defaultnotificationtime": "Default notification time", "addon.calendar.deleteallevents": "Delete all events", "addon.calendar.deleteevent": "Delete event", diff --git a/src/core/courses/providers/helper.ts b/src/core/courses/providers/helper.ts index 6ff3ba5cfe4..abd9634fe5d 100644 --- a/src/core/courses/providers/helper.ts +++ b/src/core/courses/providers/helper.ts @@ -13,9 +13,12 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { PopoverController } from 'ionic-angular'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCoursesProvider } from './courses'; import { AddonCourseCompletionProvider } from '@addon/coursecompletion/providers/coursecompletion'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; /** * Helper to gather some common courses functions. @@ -23,8 +26,46 @@ import { AddonCourseCompletionProvider } from '@addon/coursecompletion/providers @Injectable() export class CoreCoursesHelperProvider { - constructor(private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, - private courseCompletionProvider: AddonCourseCompletionProvider) { } + constructor(private coursesProvider: CoreCoursesProvider, + private utils: CoreUtilsProvider, + private courseCompletionProvider: AddonCourseCompletionProvider, + private translate: TranslateService, + private popoverCtrl: PopoverController) { } + + /** + * Get the courses to display the course picker popover. If a courseId is specified, it will also return its categoryId. + * + * @param {number} [courseId] Course ID to get the category. + * @return {Promise<{courses: any[], categoryId: number}>} Promise resolved with the list of courses and the category. + */ + getCoursesForPopover(courseId?: number): Promise<{courses: any[], categoryId: number}> { + return this.coursesProvider.getUserCourses(false).then((courses) => { + // Add "All courses". + courses.unshift({ + id: -1, + fullname: this.translate.instant('core.fulllistofcourses'), + category: -1 + }); + + let categoryId; + + if (courseId) { + // Search the course to get the category. + const course = courses.find((course) => { + return course.id == courseId; + }); + + if (course) { + categoryId = course.category; + } + } + + return { + courses: courses, + categoryId: categoryId + }; + }); + } /** * Given a course object returned by core_enrol_get_users_courses and another one returned by core_course_get_courses_by_field, @@ -174,4 +215,33 @@ export class CoreCoursesHelperProvider { }); }); } + + /** + * Show a context menu to select a course, and return the courseId and categoryId of the selected course (-1 for all courses). + * Returns an empty object if popover closed without picking a course. + * + * @param {MouseEvent} event Click event. + * @param {any[]} courses List of courses, from CoreCoursesHelperProvider.getCoursesForPopover. + * @param {number} courseId The course to select at start. + * @return {Promise<{courseId?: number, categoryId?: number}>} Promise resolved with the course ID and category ID. + */ + selectCourse(event: MouseEvent, courses: any[], courseId: number): Promise<{courseId?: number, categoryId?: number}> { + return new Promise((resolve, reject): any => { + const popover = this.popoverCtrl.create(CoreCoursePickerMenuPopoverComponent, { + courses: courses, + courseId: courseId + }); + + popover.onDidDismiss((course) => { + if (course) { + resolve({courseId: course.id, categoryId: course.category}); + } else { + resolve({}); + } + }); + popover.present({ + ev: event + }); + }); + } } From f07d6e1df7233a3b7dda4f5583fcccd6d310e901 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 18 Jul 2019 12:09:32 +0200 Subject: [PATCH 116/241] MOBILE-3021 calendar: Support links to calendar --- src/addon/calendar/calendar.module.ts | 9 +- .../calendar/addon-calendar-calendar.html | 2 +- .../calendar/components/calendar/calendar.ts | 48 +++---- .../addon-calendar-upcoming-events.html | 2 +- .../upcoming-events/upcoming-events.ts | 8 +- src/addon/calendar/pages/day/day.html | 2 +- src/addon/calendar/pages/day/day.ts | 10 +- src/addon/calendar/pages/event/event.html | 9 +- src/addon/calendar/pages/event/event.ts | 7 ++ src/addon/calendar/pages/index/index.html | 2 +- src/addon/calendar/pages/index/index.ts | 6 + src/addon/calendar/pages/list/list.ts | 9 +- src/addon/calendar/providers/calendar.ts | 118 +++++++++++++----- src/addon/calendar/providers/helper.ts | 4 +- .../calendar/providers/view-link-handler.ts | 113 +++++++++++++++++ src/providers/utils/url.ts | 11 ++ 16 files changed, 278 insertions(+), 82 deletions(-) create mode 100644 src/addon/calendar/providers/view-link-handler.ts diff --git a/src/addon/calendar/calendar.module.ts b/src/addon/calendar/calendar.module.ts index 66ab9f23717..28765fad695 100644 --- a/src/addon/calendar/calendar.module.ts +++ b/src/addon/calendar/calendar.module.ts @@ -19,12 +19,14 @@ import { AddonCalendarHelperProvider } from './providers/helper'; import { AddonCalendarSyncProvider } from './providers/calendar-sync'; import { AddonCalendarMainMenuHandler } from './providers/mainmenu-handler'; import { AddonCalendarSyncCronHandler } from './providers/sync-cron-handler'; +import { AddonCalendarViewLinkHandler } from './providers/view-link-handler'; import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; import { CoreCronDelegate } from '@providers/cron'; import { CoreInitDelegate } from '@providers/init'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreLoginHelperProvider } from '@core/login/providers/helper'; import { CoreUpdateManagerProvider } from '@providers/update-manager'; +import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; // List of providers (without handlers). export const ADDON_CALENDAR_PROVIDERS: any[] = [ @@ -45,17 +47,20 @@ export const ADDON_CALENDAR_PROVIDERS: any[] = [ AddonCalendarHelperProvider, AddonCalendarSyncProvider, AddonCalendarMainMenuHandler, - AddonCalendarSyncCronHandler + AddonCalendarSyncCronHandler, + AddonCalendarViewLinkHandler ] }) export class AddonCalendarModule { constructor(mainMenuDelegate: CoreMainMenuDelegate, calendarHandler: AddonCalendarMainMenuHandler, initDelegate: CoreInitDelegate, calendarProvider: AddonCalendarProvider, loginHelper: CoreLoginHelperProvider, localNotificationsProvider: CoreLocalNotificationsProvider, updateManager: CoreUpdateManagerProvider, - cronDelegate: CoreCronDelegate, syncHandler: AddonCalendarSyncCronHandler) { + cronDelegate: CoreCronDelegate, syncHandler: AddonCalendarSyncCronHandler, + contentLinksDelegate: CoreContentLinksDelegate, viewLinkHandler: AddonCalendarViewLinkHandler) { mainMenuDelegate.registerHandler(calendarHandler); cronDelegate.register(syncHandler); + contentLinksDelegate.registerHandler(viewLinkHandler); initDelegate.ready().then(() => { calendarProvider.scheduleAllSitesEventsNotifications(); diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index 7c607ca636f..ea373d29714 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -40,7 +40,7 @@

- + {{ event.timestart * 1000 | coreFormatDate: timeFormat }} diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 2b40b8b3f0b..2a94768f825 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -91,7 +91,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest const now = new Date(); this.year = this.initialYear ? Number(this.initialYear) : now.getFullYear(); - this.month = this.initialYear ? Number(this.initialYear) : now.getMonth() + 1; + this.month = this.initialMonth ? Number(this.initialMonth) : now.getMonth() + 1; this.canNavigate = typeof this.canNavigate == 'undefined' ? true : this.utils.isTrueOrOne(this.canNavigate); this.fetchData(); @@ -328,31 +328,33 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest protected mergeEvents(): void { const monthOfflineEvents = this.offlineEvents[this.calendarHelper.getMonthId(this.year, this.month)]; - if (!monthOfflineEvents && !this.deletedEvents.length) { - // No offline events, nothing to merge. - return; - } - this.weeks.forEach((week) => { week.days.forEach((day) => { - if (this.deletedEvents.length) { - // Mark as deleted the events that were deleted in offline. - day.events.forEach((event) => { - event.deleted = this.deletedEvents.indexOf(event.id) != -1; - }); - } - - if (this.offlineEditedEventsIds.length) { - // Remove the online events that were modified in offline. - day.events = day.events.filter((event) => { - return this.offlineEditedEventsIds.indexOf(event.id) == -1; - }); - } - - if (monthOfflineEvents && monthOfflineEvents[day.mday]) { - // Add the offline events (either new or edited). - day.events = this.sortEvents(day.events.concat(monthOfflineEvents[day.mday])); + // Format online events. + day.events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + + if (monthOfflineEvents || this.deletedEvents.length) { + // There is offline data, merge it. + + if (this.deletedEvents.length) { + // Mark as deleted the events that were deleted in offline. + day.events.forEach((event) => { + event.deleted = this.deletedEvents.indexOf(event.id) != -1; + }); + } + + if (this.offlineEditedEventsIds.length) { + // Remove the online events that were modified in offline. + day.events = day.events.filter((event) => { + return this.offlineEditedEventsIds.indexOf(event.id) == -1; + }); + } + + if (monthOfflineEvents && monthOfflineEvents[day.mday]) { + // Add the offline events (either new or edited). + day.events = this.sortEvents(day.events.concat(monthOfflineEvents[day.mday])); + } } }); }); diff --git a/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html b/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html index b338f0ecaf1..0a4b7bb1ecc 100644 --- a/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html +++ b/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html @@ -8,7 +8,7 @@

-

+

{{ 'core.notsent' | translate }} diff --git a/src/addon/calendar/components/upcoming-events/upcoming-events.ts b/src/addon/calendar/components/upcoming-events/upcoming-events.ts index f358b57857f..d362b6d6e8f 100644 --- a/src/addon/calendar/components/upcoming-events/upcoming-events.ts +++ b/src/addon/calendar/components/upcoming-events/upcoming-events.ts @@ -146,6 +146,8 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, fetchEvents(): Promise { // Don't pass courseId and categoryId, we'll filter them locally. return this.calendarProvider.getUpcomingEvents().then((result) => { + const promises = []; + this.onlineEvents = result.events; this.onlineEvents.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); @@ -158,8 +160,12 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, // Re-calculate the formatted time so it uses the device date. this.events.forEach((event) => { - event.formattedtime = this.calendarProvider.formatEventTime(event, this.timeFormat); + promises.push(this.calendarProvider.formatEventTime(event, this.timeFormat).then((time) => { + event.formattedtime = time; + })); }); + + return Promise.all(promises); }); } diff --git a/src/addon/calendar/pages/day/day.html b/src/addon/calendar/pages/day/day.html index e9b48bd5261..134def7365f 100644 --- a/src/addon/calendar/pages/day/day.html +++ b/src/addon/calendar/pages/day/day.html @@ -49,7 +49,7 @@

-

+

{{ 'core.notsent' | translate }} diff --git a/src/addon/calendar/pages/day/day.ts b/src/addon/calendar/pages/day/day.ts index 924bfa0e74b..43be54b5b81 100644 --- a/src/addon/calendar/pages/day/day.ts +++ b/src/addon/calendar/pages/day/day.ts @@ -188,7 +188,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { * View loaded. */ ngOnInit(): void { - this.currentMoment = moment().year(this.year).month(this.month - 1).day(this.day); + this.currentMoment = moment().year(this.year).month(this.month - 1).date(this.day); this.fetchData(true, false); } @@ -273,6 +273,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { fetchEvents(): Promise { // Don't pass courseId and categoryId, we'll filter them locally. return this.calendarProvider.getDayEvents(this.year, this.month, this.day).then((result) => { + const promises = []; // Calculate the period name. We don't use the one in result because it's in server's language. this.periodName = this.timeUtils.userDate(new Date(this.year, this.month - 1, this.day).getTime(), @@ -288,9 +289,14 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.filterEvents(); // Re-calculate the formatted time so it uses the device date. + const dayTime = this.currentMoment.unix() * 1000; this.events.forEach((event) => { - event.formattedtime = this.calendarProvider.formatEventTime(event, this.timeFormat); + promises.push(this.calendarProvider.formatEventTime(event, this.timeFormat, true, dayTime).then((time) => { + event.formattedtime = time; + })); }); + + return Promise.all(promises); }); } diff --git a/src/addon/calendar/pages/event/event.html b/src/addon/calendar/pages/event/event.html index 23e55aac7ae..6e64b6fe0df 100644 --- a/src/addon/calendar/pages/event/event.html +++ b/src/addon/calendar/pages/event/event.html @@ -29,18 +29,11 @@

+

{{ 'core.deletedoffline' | translate }}
- -

{{ 'addon.calendar.eventstarttime' | translate}}

-

{{ event.timestart * 1000 | coreFormatDate }}

-
- -

{{ 'addon.calendar.eventendtime' | translate}}

-

{{ (event.timestart + event.timeduration) * 1000 | coreFormatDate }}

-

{{ 'core.course' | translate}}

diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index bfb45499c34..7af26515225 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -311,6 +311,13 @@ export class AddonCalendarEventPage implements OnDestroy { event.deleted = deleted; })); + // Re-calculate the formatted time so it uses the device date. + promises.push(this.calendarProvider.getCalendarTimeFormat().then((timeFormat) => { + this.calendarProvider.formatEventTime(event, timeFormat).then((time) => { + event.formattedtime = time; + }); + })); + return Promise.all(promises); }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevent', true); diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html index 099892df88e..ca8e832f3ad 100644 --- a/src/addon/calendar/pages/index/index.html +++ b/src/addon/calendar/pages/index/index.html @@ -28,7 +28,7 @@ {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }} - + diff --git a/src/addon/calendar/pages/index/index.ts b/src/addon/calendar/pages/index/index.ts index 59088156bff..341cf36b3bb 100644 --- a/src/addon/calendar/pages/index/index.ts +++ b/src/addon/calendar/pages/index/index.ts @@ -53,6 +53,8 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { protected manualSyncObserver: any; protected onlineObserver: any; + year: number; + month: number; courseId: number; categoryId: number; canCreate = false; @@ -82,8 +84,12 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { this.courseId = navParams.get('courseId'); this.eventId = navParams.get('eventId') || false; + this.year = navParams.get('year'); + this.month = navParams.get('month'); this.notificationsEnabled = localNotificationsProvider.isAvailable(); this.currentSiteId = sitesProvider.getCurrentSiteId(); + this.loadUpcoming = !!navParams.get('upcoming'); + this.showCalendar = !this.loadUpcoming; // Listen for events added. When an event is added, reload the data. this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index 1c7572e1236..0b864a78ab9 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -53,7 +53,6 @@ export class AddonCalendarListPage implements OnDestroy { protected siteHomeId: number; protected obsDefaultTimeChange: any; protected eventId: number; - protected preSelectedCourseId: number; protected newEventObserver: any; protected discardedObserver: any; protected editEventObserver: any; @@ -101,7 +100,7 @@ export class AddonCalendarListPage implements OnDestroy { } this.eventId = navParams.get('eventId') || false; - this.preSelectedCourseId = navParams.get('courseId') || null; + this.courseId = navParams.get('courseId'); // Listen for events added. When an event is added, reload the data. this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { @@ -284,12 +283,6 @@ export class AddonCalendarListPage implements OnDestroy { this.courses = result.courses; this.categoryId = result.categoryId; - if (this.preSelectedCourseId) { - this.filter.course = courses.find((course) => { - return course.id == this.preSelectedCourseId; - }); - } - return this.fetchEvents(refresh); })); diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 3bb8680347c..e7c0800f3c1 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -18,7 +18,9 @@ import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreSite } from '@classes/site'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreAppProvider } from '@providers/app'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreGroupsProvider } from '@providers/groups'; import { CoreConstants } from '@core/constants'; @@ -259,7 +261,9 @@ export class AddonCalendarProvider { private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider, private coursesProvider: CoreCoursesProvider, + private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider, + private urlUtils: CoreUrlUtilsProvider, private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider, private utils: CoreUtilsProvider, @@ -487,15 +491,19 @@ export class AddonCalendarProvider { } /** - * Format event time. Equivalent to calendar_format_event_time. + * Format event time. Similar to calendar_format_event_time. * * @param {any} event Event to format. * @param {string} format Calendar time format (from getCalendarTimeFormat). * @param {boolean} [useCommonWords=true] Whether to use common words like "Today", "Yesterday", etc. + * @param {number} [seenDay] Timestamp of day currently seen. If set, the function will not add links to this day. * @param {number} [showTime=0] Determine the show time GMT timestamp. - * @return {string} Formatted event time. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the formatted event time. */ - formatEventTime(event: any, format: string, useCommonWords: boolean = true, showTime: number = 0): string { + formatEventTime(event: any, format: string, useCommonWords: boolean = true, seenDay?: number, showTime: number = 0, + siteId?: string): Promise { + const start = event.timestart * 1000, end = (event.timestart + event.timeduration) * 1000; let time; @@ -511,46 +519,50 @@ export class AddonCalendarProvider { this.timeUtils.userDate(end, format); } - if (!showTime) { - return this.getDayRepresentation(start, useCommonWords) + ', ' + time; - } else { - return time; - } - } else { // Event lasts more than one day. - const midnightStart = moment(start).startOf('day').unix() * 1000, - midnightEnd = moment(end).startOf('day').unix() * 1000, - timeStart = this.timeUtils.userDate(start, format), - timeEnd = this.timeUtils.userDate(end, format); - let dayStart = this.getDayRepresentation(start, useCommonWords) + ', ', - dayEnd = this.getDayRepresentation(end, useCommonWords) + ', '; - - if (showTime == midnightStart) { - dayStart = ''; + const timeStart = this.timeUtils.userDate(start, format), + timeEnd = this.timeUtils.userDate(end, format), + promises = []; + + // Don't use common words when the event lasts more than one day. + let dayStart = this.getDayRepresentation(start, false) + ', ', + dayEnd = this.getDayRepresentation(end, false) + ', '; + + // Add links to the days if needed. + if (dayStart && (!seenDay || !moment(seenDay).isSame(start, 'day'))) { + promises.push(this.getViewUrl('day', event.timestart, undefined, siteId).then((url) => { + dayStart = this.urlUtils.buildLink(url, dayStart); + })); } - - if (showTime == midnightEnd) { - dayEnd = ''; + if (dayEnd && (!seenDay || !moment(seenDay).isSame(end, 'day'))) { + promises.push(this.getViewUrl('day', end / 1000, undefined, siteId).then((url) => { + dayEnd = this.urlUtils.buildLink(url, dayEnd); + })); } - // Set printable representation. - if (moment().isSame(start, 'day')) { - // Event starts today, don't display the day. - return timeStart + ' » ' + dayEnd + timeEnd; - } else { - // The event starts in the future, print both days. + return Promise.all(promises).then(() => { return dayStart + timeStart + ' » ' + dayEnd + timeEnd; - } + }); } - } else { // There is no time duration. - const time = this.timeUtils.userDate(start, format); + } else { + // There is no time duration. + time = this.timeUtils.userDate(start, format); + } - if (!showTime) { - return this.getDayRepresentation(start, useCommonWords) + ', ' + time.trim(); + if (!showTime) { + // Display day + time. + if (seenDay && moment(seenDay).isSame(start, 'day')) { + // This day is currently being displayed, don't add an link. + return Promise.resolve(this.getDayRepresentation(start, useCommonWords) + ', ' + time); } else { - return time; + // Add link to view the day. + return this.getViewUrl('day', event.timestart, undefined, siteId).then((url) => { + return this.urlUtils.buildLink(url, this.getDayRepresentation(start, useCommonWords)) + ', ' + time; + }); } + } else { + return Promise.resolve(time); } } @@ -841,6 +853,21 @@ export class AddonCalendarProvider { }); } + /** + * Return the normalised event type. + * Activity events are normalised to be course events. + * + * @param {any} event The event to get its type. + * @return {string} Event type. + */ + getEventType(event: any): string { + if (event.modulename) { + return 'course'; + } + + return event.eventtype; + } + /** * Remove an event reminder and cancel the notification. * @@ -1155,6 +1182,31 @@ export class AddonCalendarProvider { return this.getUpcomingEventsPrefixCacheKey() + (courseId ? courseId : '') + ':' + (categoryId ? categoryId : ''); } + /** + * Get URL to view a calendar. + * + * @param {string} view The view to load: 'month', 'day', 'upcoming', etc. + * @param {number} [time] Time to load. If not defined, current time. + * @param {string} [courseId] Course to load. If not defined, all courses. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the URL.x + */ + getViewUrl(view: string, time?: number, courseId?: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + let url = this.textUtils.concatenatePaths(site.getURL(), 'calendar/view.php?view=' + view); + + if (time) { + url += '&time=' + time; + } + + if (courseId) { + url += '&course=' + courseId; + } + + return url; + }); + } + /** * Get the week days, already ordered according to a specified starting day. * diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index 225ec7ffb98..575500c1628 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -54,7 +54,7 @@ export class AddonCalendarHelperProvider { const types = {}; events.forEach((event) => { - types[event.eventtype] = true; + types[event.formattedType || event.eventtype] = true; if (event.islastday) { day.haslastdayofevent = true; @@ -133,6 +133,8 @@ export class AddonCalendarHelperProvider { e.moduleIcon = e.icon; } + e.formattedType = this.calendarProvider.getEventType(e); + if (typeof e.duration != 'undefined') { // It's an offline event, add some calculated data. e.format = 1; diff --git a/src/addon/calendar/providers/view-link-handler.ts b/src/addon/calendar/providers/view-link-handler.ts new file mode 100644 index 00000000000..e0ba641c072 --- /dev/null +++ b/src/addon/calendar/providers/view-link-handler.ts @@ -0,0 +1,113 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { AddonCalendarProvider } from './calendar'; + +/** + * Content links handler for calendar view page. + */ +@Injectable() +export class AddonCalendarViewLinkHandler extends CoreContentLinksHandlerBase { + name = 'AddonCalendarViewLinkHandler'; + pattern = /\/calendar\/view\.php/; + + protected SUPPORTED_VIEWS = ['month', 'mini', 'minithree', 'day', 'upcoming', 'upcoming_mini']; + + constructor(private linkHelper: CoreContentLinksHelperProvider, private calendarProvider: AddonCalendarProvider) { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + return [{ + action: (siteId, navCtrl?): void => { + if (!params.view || params.view == 'month' || params.view == 'mini' || params.view == 'minithree') { + // Monthly view, open the calendar tab. + const stateParams: any = { + courseId: params.course + }, + timestamp = params.time ? params.time * 1000 : Date.now(); + + const date = new Date(timestamp); + stateParams.year = date.getFullYear(); + stateParams.month = date.getMonth() + 1; + + this.linkHelper.goInSite(navCtrl, 'AddonCalendarIndexPage', stateParams, siteId); // @todo: Add checkMenu param. + + } else if (params.view == 'day') { + // Daily view, open the page. + const stateParams: any = { + courseId: params.course + }, + timestamp = params.time ? params.time * 1000 : Date.now(); + + const date = new Date(timestamp); + stateParams.year = date.getFullYear(); + stateParams.month = date.getMonth() + 1; + stateParams.day = date.getDate(); + + this.linkHelper.goInSite(navCtrl, 'AddonCalendarDayPage', stateParams, siteId); + + } else if (params.view == 'upcoming' || params.view == 'upcoming_mini') { + // Upcoming view, open the calendar tab. + const stateParams: any = { + courseId: params.course, + upcoming: true, + }; + + this.linkHelper.goInSite(navCtrl, 'AddonCalendarIndexPage', stateParams, siteId); // @todo: Add checkMenu param. + + } + } + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + if (params.view && this.SUPPORTED_VIEWS.indexOf(params.view) == -1) { + // This type of view isn't supported in the app. + return false; + } + + return this.calendarProvider.isDisabled(siteId).then((disabled) => { + if (disabled) { + return false; + } + + return this.calendarProvider.canViewMonth(siteId); + }); + } +} diff --git a/src/providers/utils/url.ts b/src/providers/utils/url.ts index 3035f53c847..70bd15d55e1 100644 --- a/src/providers/utils/url.ts +++ b/src/providers/utils/url.ts @@ -44,6 +44,17 @@ export class CoreUrlUtilsProvider { return url; } + /** + * Given a URL and a text, return an HTML link. + * + * @param {string} url URL. + * @param {string} text Text of the link. + * @return {string} Link. + */ + buildLink(url: string, text: string): string { + return '
' + text + ''; + } + /** * Extracts the parameters from a URL and stores them in an object. * From 6d94c961f17634aba51327c815c20942557db9b3 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 18 Jul 2019 12:56:55 +0200 Subject: [PATCH 117/241] MOBILE-3021 calendar: Improve event page display --- scripts/langindex.json | 6 ++-- .../calendar/addon-calendar-calendar.html | 1 + .../components/calendar/calendar.scss | 7 +++++ src/addon/calendar/lang/en.json | 1 + src/addon/calendar/pages/event/event.html | 28 ++++++++++++++----- src/addon/calendar/pages/event/event.ts | 25 ++++------------- src/assets/lang/en.json | 1 + 7 files changed, 41 insertions(+), 28 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index d80355727c2..aa47c88214a 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -30,6 +30,7 @@ "addon.block_badges.pluginname": "block_badges", "addon.block_blogmenu.pluginname": "block_blog_menu", "addon.block_blogrecent.nocourses": "block_blog_recent", + "addon.block_blogrecent.pluginname": "block_blog_recent", "addon.block_blogtags.pluginname": "block_blog_tags", "addon.block_calendarmonth.pluginname": "block_calendar_month", "addon.block_calendarupcoming.pluginname": "block_calendar_upcoming", @@ -51,12 +52,12 @@ "addon.block_newsitems.pluginname": "block_news_items", "addon.block_onlineusers.pluginname": "block_online_users", "addon.block_privatefiles.pluginname": "block_private_files", + "addon.block_recentactivity.pluginname": "block_recent_activity", "addon.block_recentlyaccessedcourses.nocourses": "block_recentlyaccessedcourses", "addon.block_recentlyaccessedcourses.pluginname": "block_recentlyaccessedcourses", "addon.block_recentlyaccesseditems.noitems": "block_recentlyaccesseditems", - "addon.block_recentactivity.pluginname": "block_recent_activity", + "addon.block_recentlyaccesseditems.pluginname": "block_recentlyaccesseditems", "addon.block_rssclient.pluginname": "block_rss_client", - "addon.block_glossaryrandom.pluginname": "block_glossary_random", "addon.block_selfcompletion.pluginname": "block_selfcompletion", "addon.block_sitemainmenu.pluginname": "block_site_main_menu", "addon.block_starredcourses.nocourses": "block_starredcourses", @@ -149,6 +150,7 @@ "addon.calendar.upcomingevents": "calendar", "addon.calendar.wed": "calendar", "addon.calendar.wednesday": "calendar", + "addon.calendar.when": "calendar", "addon.calendar.yesterday": "calendar", "addon.competency.activities": "tool_lp", "addon.competency.competencies": "competency", diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index ea373d29714..1cc977fdf5b 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -43,6 +43,7 @@ + {{ event.timestart * 1000 | coreFormatDate: timeFormat }} {{event.name}}

diff --git a/src/addon/calendar/components/calendar/calendar.scss b/src/addon/calendar/components/calendar/calendar.scss index af7182c321c..b764187e44a 100644 --- a/src/addon/calendar/components/calendar/calendar.scss +++ b/src/addon/calendar/components/calendar/calendar.scss @@ -48,4 +48,11 @@ ion-app.app-root addon-calendar-calendar { background-color: $calendar-event-site-color; } } + + .core-module-icon { + @include margin-horizontal(1px, 1px); + width: 16px; + height: 16px; + display: inline; + } } \ No newline at end of file diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index 8d19b455941..be2bab15ac5 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -65,5 +65,6 @@ "upcomingevents": "Upcoming events", "wed": "Wed", "wednesday": "Wednesday", + "when": "When", "yesterday": "Yesterday" } \ No newline at end of file diff --git a/src/addon/calendar/pages/event/event.html b/src/addon/calendar/pages/event/event.html index 6e64b6fe0df..366327f6661 100644 --- a/src/addon/calendar/pages/event/event.html +++ b/src/addon/calendar/pages/event/event.html @@ -1,6 +1,10 @@ - + + + + + @@ -26,14 +30,26 @@ - + + -

-

+

{{ 'addon.calendar.eventname' | translate }}

+

{{ 'core.deletedoffline' | translate }}
+ +

{{ 'addon.calendar.when' | translate }}

+

+ + {{ 'core.deletedoffline' | translate }} + +
+ +

{{ 'addon.calendar.eventtype' | translate }}

+

{{ 'addon.calendar.type' + event.formattedType | translate }}

+

{{ 'core.course' | translate}}

@@ -46,10 +62,8 @@

{{ 'core.group' | translate}}

{{ 'core.category' | translate}}

- - {{event.moduleName}} - +

{{ 'core.description' | translate}}

diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index 7af26515225..fb935474828 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -57,7 +57,6 @@ export class AddonCalendarEventPage implements OnDestroy { notificationMax: string; notificationTimeText: string; event: any = {}; - title: string; courseName: string; groupName: string; courseUrl = ''; @@ -236,8 +235,6 @@ export class AddonCalendarEventPage implements OnDestroy { this.courseUrl = ''; this.moduleUrl = ''; - // Guess event title. - let title = this.translate.instant('addon.calendar.type' + event.eventtype); if (event.moduleIcon) { // It's a module event, translate the module name to the current language. const name = this.courseProvider.translateModuleName(event.modulename); @@ -245,27 +242,12 @@ export class AddonCalendarEventPage implements OnDestroy { event.moduleName = name; } - // Calculate the title of the page; - if (title == 'addon.calendar.type' + event.eventtype) { - title = this.translate.instant('core.mod_' + event.modulename + '.' + event.eventtype); - - if (title == 'core.mod_' + event.modulename + '.' + event.eventtype) { - title = name; - } - } - // Get the module URL. if (canGetById) { this.moduleUrl = event.url; } - } else { - if (title == 'addon.calendar.type' + event.eventtype) { - title = event.name; - } } - this.title = title; - // If the event belongs to a course, get the course name and the URL to view it. if (canGetById && event.course && event.course.id != this.siteHomeId) { this.courseName = event.course.fullname; @@ -403,7 +385,12 @@ export class AddonCalendarEventPage implements OnDestroy { refreshEvent(sync?: boolean, showErrors?: boolean): Promise { this.syncIcon = 'spinner'; - return this.calendarProvider.invalidateEvent(this.eventId).catch(() => { + const promises = []; + + promises.push(this.calendarProvider.invalidateEvent(this.eventId)); + promises.push(this.calendarProvider.invalidateTimeFormat()); + + return Promise.all(promises).catch(() => { // Ignore errors. }).then(() => { return this.fetchEvent(sync, showErrors); diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index b78160d84c5..a8564ee9209 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -149,6 +149,7 @@ "addon.calendar.upcomingevents": "Upcoming events", "addon.calendar.wed": "Wed", "addon.calendar.wednesday": "Wednesday", + "addon.calendar.when": "When", "addon.calendar.yesterday": "Yesterday", "addon.competency.activities": "Activities", "addon.competency.competencies": "Competencies", From 31c92398b3e884a25d16d06c3ae26ffc959b29d6 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 18 Jul 2019 17:22:51 +0200 Subject: [PATCH 118/241] MOBILE-3021 calendar: Schedule event notifications --- .../calendar/components/calendar/calendar.ts | 18 ++++++++++++++++++ .../upcoming-events/upcoming-events.ts | 14 ++++++++++++++ src/addon/calendar/pages/day/day.ts | 12 ++++++++++++ 3 files changed, 44 insertions(+) diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 2a94768f825..cbeeb637948 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -14,6 +14,7 @@ import { Component, OnDestroy, OnInit, Input, OnChanges, SimpleChange, Output, EventEmitter } from '@angular/core'; import { CoreEventsProvider } from '@providers/events'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -56,9 +57,11 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest // Observers. protected undeleteEventObserver: any; + protected obsDefaultTimeChange: any; constructor(eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, + localNotificationsProvider: CoreLocalNotificationsProvider, private calendarProvider: AddonCalendarProvider, private calendarHelper: AddonCalendarHelperProvider, private calendarOffline: AddonCalendarOfflineProvider, @@ -69,6 +72,17 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.currentSiteId = sitesProvider.getCurrentSiteId(); + if (localNotificationsProvider.isAvailable()) { + // Re-schedule events if default time changes. + this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => { + this.weeks.forEach((week) => { + week.days.forEach((day) => { + calendarProvider.scheduleEventsNotifications(day.events); + }); + }); + }, this.currentSiteId); + } + // Listen for events "undeleted" (offline). this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => { if (data && data.eventId) { @@ -334,6 +348,9 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest // Format online events. day.events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + // Schedule notifications for the events retrieved (only future events will be scheduled). + this.calendarProvider.scheduleEventsNotifications(day.events); + if (monthOfflineEvents || this.deletedEvents.length) { // There is offline data, merge it. @@ -403,5 +420,6 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest */ ngOnDestroy(): void { this.undeleteEventObserver && this.undeleteEventObserver.off(); + this.obsDefaultTimeChange && this.obsDefaultTimeChange.off(); } } diff --git a/src/addon/calendar/components/upcoming-events/upcoming-events.ts b/src/addon/calendar/components/upcoming-events/upcoming-events.ts index d362b6d6e8f..d540dae6e8f 100644 --- a/src/addon/calendar/components/upcoming-events/upcoming-events.ts +++ b/src/addon/calendar/components/upcoming-events/upcoming-events.ts @@ -14,6 +14,7 @@ import { Component, OnDestroy, OnInit, Input, OnChanges, SimpleChange, Output, EventEmitter } from '@angular/core'; import { CoreEventsProvider } from '@providers/events'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { AddonCalendarProvider } from '../../providers/calendar'; @@ -51,9 +52,11 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, // Observers. protected undeleteEventObserver: any; + protected obsDefaultTimeChange: any; constructor(eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, + localNotificationsProvider: CoreLocalNotificationsProvider, private calendarProvider: AddonCalendarProvider, private calendarHelper: AddonCalendarHelperProvider, private calendarOffline: AddonCalendarOfflineProvider, @@ -62,6 +65,13 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, this.currentSiteId = sitesProvider.getCurrentSiteId(); + if (localNotificationsProvider.isAvailable()) { + // Re-schedule events if default time changes. + this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => { + calendarProvider.scheduleEventsNotifications(this.onlineEvents); + }, this.currentSiteId); + } + // Listen for events "undeleted" (offline). this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => { if (data && data.eventId) { @@ -152,6 +162,9 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, this.onlineEvents.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + // Schedule notifications for the events retrieved. + this.calendarProvider.scheduleEventsNotifications(this.onlineEvents); + // Merge the online events with offline data. this.events = this.mergeEvents(); @@ -319,5 +332,6 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, */ ngOnDestroy(): void { this.undeleteEventObserver && this.undeleteEventObserver.off(); + this.obsDefaultTimeChange && this.obsDefaultTimeChange.off(); } } diff --git a/src/addon/calendar/pages/day/day.ts b/src/addon/calendar/pages/day/day.ts index 43be54b5b81..d18e25d75ea 100644 --- a/src/addon/calendar/pages/day/day.ts +++ b/src/addon/calendar/pages/day/day.ts @@ -61,6 +61,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { protected syncObserver: any; protected manualSyncObserver: any; protected onlineObserver: any; + protected obsDefaultTimeChange: any; periodName: string; filteredEvents = []; @@ -98,6 +99,13 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.courseId = navParams.get('courseId'); this.currentSiteId = sitesProvider.getCurrentSiteId(); + if (localNotificationsProvider.isAvailable()) { + // Re-schedule events if default time changes. + this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => { + calendarProvider.scheduleEventsNotifications(this.onlineEvents); + }, this.currentSiteId); + } + // Listen for events added. When an event is added, reload the data. this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { if (data && data.event) { @@ -282,6 +290,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.onlineEvents = result.events; this.onlineEvents.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + // Schedule notifications for the events retrieved (only future events will be scheduled). + this.calendarProvider.scheduleEventsNotifications(this.onlineEvents); + // Merge the online events with offline data. this.events = this.mergeEvents(); @@ -609,5 +620,6 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.syncObserver && this.syncObserver.off(); this.manualSyncObserver && this.manualSyncObserver.off(); this.onlineObserver && this.onlineObserver.unsubscribe(); + this.obsDefaultTimeChange && this.obsDefaultTimeChange.off(); } } From d7d565086b5ff96d6c019c31b43467a17c109764 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 19 Jul 2019 09:07:53 +0200 Subject: [PATCH 119/241] MOBILE-3021 calendar: Fix calendar block links --- .../calendarmonth/providers/block-handler.ts | 14 +++++++++++--- .../calendarupcoming/providers/block-handler.ts | 15 ++++++++++++--- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/addon/block/calendarmonth/providers/block-handler.ts b/src/addon/block/calendarmonth/providers/block-handler.ts index 9dc4ce2ab5e..80e85edf8b1 100644 --- a/src/addon/block/calendarmonth/providers/block-handler.ts +++ b/src/addon/block/calendarmonth/providers/block-handler.ts @@ -16,6 +16,7 @@ import { Injectable, Injector } from '@angular/core'; import { CoreBlockHandlerData } from '@core/block/providers/delegate'; import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block'; import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; +import { AddonCalendarProvider } from '@addon/calendar/providers/calendar'; /** * Block handler. @@ -25,7 +26,7 @@ export class AddonBlockCalendarMonthHandler extends CoreBlockBaseHandler { name = 'AddonBlockCalendarMonth'; blockName = 'calendar_month'; - constructor() { + constructor(private calendarProvider: AddonCalendarProvider) { super(); } @@ -41,12 +42,19 @@ export class AddonBlockCalendarMonthHandler extends CoreBlockBaseHandler { getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) : CoreBlockHandlerData | Promise { + let link = 'AddonCalendarListPage'; + const linkParams: any = contextLevel == 'course' ? { courseId: instanceId } : {}; + + if (this.calendarProvider.canViewMonthInSite()) { + link = 'AddonCalendarIndexPage'; + } + return { title: 'addon.block_calendarmonth.pluginname', class: 'addon-block-calendar-month', component: CoreBlockOnlyTitleComponent, - link: 'AddonCalendarListPage', - linkParams: contextLevel == 'course' ? { courseId: instanceId } : null + link: link, + linkParams: linkParams }; } } diff --git a/src/addon/block/calendarupcoming/providers/block-handler.ts b/src/addon/block/calendarupcoming/providers/block-handler.ts index b7f4e8acdd2..c58a0d08038 100644 --- a/src/addon/block/calendarupcoming/providers/block-handler.ts +++ b/src/addon/block/calendarupcoming/providers/block-handler.ts @@ -16,6 +16,7 @@ import { Injectable, Injector } from '@angular/core'; import { CoreBlockHandlerData } from '@core/block/providers/delegate'; import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block'; import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; +import { AddonCalendarProvider } from '@addon/calendar/providers/calendar'; /** * Block handler. @@ -25,7 +26,7 @@ export class AddonBlockCalendarUpcomingHandler extends CoreBlockBaseHandler { name = 'AddonBlockCalendarUpcoming'; blockName = 'calendar_upcoming'; - constructor() { + constructor(private calendarProvider: AddonCalendarProvider) { super(); } @@ -41,12 +42,20 @@ export class AddonBlockCalendarUpcomingHandler extends CoreBlockBaseHandler { getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) : CoreBlockHandlerData | Promise { + let link = 'AddonCalendarListPage'; + const linkParams: any = contextLevel == 'course' ? { courseId: instanceId } : {}; + + if (this.calendarProvider.canViewMonthInSite()) { + link = 'AddonCalendarIndexPage'; + linkParams.upcoming = true; + } + return { title: 'addon.block_calendarupcoming.pluginname', class: 'addon-block-calendar-upcoming', component: CoreBlockOnlyTitleComponent, - link: 'AddonCalendarListPage', - linkParams: contextLevel == 'course' ? { courseId: instanceId } : null + link: link, + linkParams: linkParams }; } } From 762dc4b0f686107a6d608027a270e7bdaee6f777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 19 Jul 2019 11:42:22 +0200 Subject: [PATCH 120/241] MOBILE-3021 calendar: Add styling to monthly view --- .../calendar/addon-calendar-calendar.html | 22 +++--- .../components/calendar/calendar.scss | 76 ++++++++++++++++++- src/addon/calendar/pages/day/day.html | 4 +- src/addon/calendar/pages/day/day.scss | 9 +++ 4 files changed, 96 insertions(+), 15 deletions(-) create mode 100644 src/addon/calendar/pages/day/day.scss diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index 1cc977fdf5b..679b0996edf 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -7,8 +7,8 @@ - -

{{ periodName }}

+ +

{{ periodName }}

@@ -19,22 +19,22 @@ - + - +

{{ day.shortname | translate }}

- - - + + +

{{ day.mday }}

-

+

@@ -44,14 +44,14 @@ - {{ event.timestart * 1000 | coreFormatDate: timeFormat }} - {{event.name}} + {{ event.timestart * 1000 | coreFormatDate: timeFormat }} + {{event.name}}

{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}

- +
diff --git a/src/addon/calendar/components/calendar/calendar.scss b/src/addon/calendar/components/calendar/calendar.scss index b764187e44a..f96cba14d4f 100644 --- a/src/addon/calendar/components/calendar/calendar.scss +++ b/src/addon/calendar/components/calendar/calendar.scss @@ -4,11 +4,81 @@ $calendar-event-course-color: $red !default; // Red. $calendar-event-group-color: $yellow !default; // Yellow. $calendar-event-user-color: $blue !default; // Blue. $calendar-event-site-color: $green !default; // Green. +$calendar-today-bgcolor: $core-color !default; +$calendar-today-color: $white !default; +$calendar-border-color: $gray !default; ion-app.app-root addon-calendar-calendar { - .addon-calendar-weekdays { - opacity: 0.4; + .addon-calendar-months { + background-color: white; + padding: 0; + } + + .addon-calendar-day { + border-bottom: 1px solid $calendar-border-color; + @include border-end(1px, solid, $calendar-border-color); + overflow: hidden; + min-height: 70px; + + &:first-child { + @include padding(null, null, null, 10px); + } + &:last-child { + @include border-end(0, null, null); + @include padding(null, 8px, null, null); + } + .addon-calendar-day-number { + height: 24px; + line-height: 24px; + width: max-content; + min-width: 24px; + text-align: center; + font-weight: 500; + display: inline-block; + margin: 3px; + } + &.today .addon-calendar-day-number { + background-color: $calendar-today-bgcolor; + color: $calendar-today-color; + + border-radius: 50%; + } + &.dayblank { + background-color: $gray-lighter; + } + + .addon-calendar-event { + margin-top: 0.6em; + margin-bottom: 0.6em; + overflow: hidden; + white-space: nowrap; + + .addon-calendar-event-name { + font-weight: 500; + } + } + + .addon-calendar-day-more { + @include margin(0.6em, null, 0.6em, 4px); + } + + .addon-calendar-dot-types { + @include margin(0.6em, null, 0.6em, null); + } + } + + .addon-calendar-period { + flex-grow: 3; + h3 { + margin-top: 10px; + font-size: 1.6rem; + } + } + + .addon-calendar-weekday { + color: $gray-dark; + border-bottom: 1px solid $list-md-border-color; } .addon-calendar-day-events { @@ -32,6 +102,8 @@ ion-app.app-root addon-calendar-calendar { border: 1px solid white; @include margin-horizontal(1px, 1px); + + &.calendar_event_category { background-color: $calendar-event-category-color; } diff --git a/src/addon/calendar/pages/day/day.html b/src/addon/calendar/pages/day/day.html index 134def7365f..e6db57d369f 100644 --- a/src/addon/calendar/pages/day/day.html +++ b/src/addon/calendar/pages/day/day.html @@ -24,8 +24,8 @@
- -

{{ periodName }}

+ +

{{ periodName }}

diff --git a/src/addon/calendar/pages/day/day.scss b/src/addon/calendar/pages/day/day.scss new file mode 100644 index 00000000000..4918c1312ec --- /dev/null +++ b/src/addon/calendar/pages/day/day.scss @@ -0,0 +1,9 @@ +page-addon-calendar-day { + .addon-calendar-period { + flex-grow: 3; + h3 { + margin-top: 10px; + font-size: 1.6rem; + } + } +} \ No newline at end of file From 26157100813deb2a4a5c78961e6c67f6332a2ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 19 Jul 2019 12:41:16 +0200 Subject: [PATCH 121/241] MOBILE-3021 styles: Make it easy to change ionic colors --- src/theme/variables.scss | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/theme/variables.scss b/src/theme/variables.scss index 0062ebe0961..9b5f9ae66c5 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -35,7 +35,6 @@ $core-color: $orange; // -------------------------------------------------- @import "bmma"; - $blue-light: mix($blue, white, 20%) !default; // Background. $blue-dark: darken($blue, 10%) !default; @@ -76,19 +75,31 @@ $content-padding: 10px; // colors so you can add, rename and remove colors as needed. // The "primary" color is the only required color in the map. +$primary: $core-color !default; +$secondary: $turquoise !default; +$danger: $red !default; +$light: $gray-lighter !default; +$color-gray: $gray-dark !default; +$dark: $black !default; +$warning: $yellow !default; +$success: $green !default; +$info: $blue !default; +$inverted-base: $white !default; +$inverted-contrast: $primary !default; + $colors: ( - primary: $core-color, - secondary: $turquoise, - danger: $red, - light: $gray-lighter, - gray: $gray-dark, - dark: $black, - warning: $yellow, - success: $green, - info: $blue, + primary: $primary, + secondary: $secondary, + danger: $danger, + light: $light, + gray: $color-gray, + dark: $dark, + warning: $warning, + success: $success, + info: $info, inverted: ( - base: $white, - contrast: $core-color + base: $inverted-base, + contrast: $inverted-contrast ) ); From 12e61f1f5887aeb1234dec23ac418e6a03200a5a Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 19 Jul 2019 15:52:43 +0200 Subject: [PATCH 122/241] MOBILE-3021 calendar: Add a button to view current month or day --- .../calendar/addon-calendar-calendar.html | 8 +++ .../calendar/components/calendar/calendar.ts | 43 ++++++++++++++- src/addon/calendar/pages/day/day.html | 3 ++ src/addon/calendar/pages/day/day.ts | 52 ++++++++++++++++++- src/addon/calendar/pages/index/index.html | 4 +- 5 files changed, 106 insertions(+), 4 deletions(-) diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index 679b0996edf..5872e86d0d6 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -1,3 +1,11 @@ + + + + + + diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index cbeeb637948..dcdb412b16f 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -37,6 +37,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest @Input() courseId: number | string; @Input() categoryId: number | string; // Category ID the course belongs to. @Input() canNavigate?: string | boolean; // Whether to include arrows to change the month. Defaults to true. + @Input() displayNavButtons?: string | boolean; // Whether to display nav buttons created by this component. Defaults to true. @Output() onEventClicked = new EventEmitter(); @Output() onDayClicked = new EventEmitter<{day: number, month: number, year: number}>(); @@ -45,6 +46,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest weeks: any[]; loaded = false; timeFormat: string; + isCurrentMonth: boolean; protected year: number; protected month: number; @@ -106,7 +108,8 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.year = this.initialYear ? Number(this.initialYear) : now.getFullYear(); this.month = this.initialMonth ? Number(this.initialMonth) : now.getMonth() + 1; - this.canNavigate = typeof this.canNavigate == 'undefined' ? true : this.utils.isTrueOrOne(this.canNavigate); + + this.calculateIsCurrentMonth(); this.fetchData(); } @@ -115,6 +118,9 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest * Detect changes on input properties. */ ngOnChanges(changes: {[name: string]: SimpleChange}): void { + this.canNavigate = typeof this.canNavigate == 'undefined' ? true : this.utils.isTrueOrOne(this.canNavigate); + this.displayNavButtons = typeof this.displayNavButtons == 'undefined' ? true : + this.utils.isTrueOrOne(this.displayNavButtons); if ((changes.courseId || changes.categoryId) && this.weeks) { this.filterEvents(); @@ -274,6 +280,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); this.decreaseMonth(); }).finally(() => { + this.calculateIsCurrentMonth(); this.loaded = true; }); } @@ -290,6 +297,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); this.increaseMonth(); }).finally(() => { + this.calculateIsCurrentMonth(); this.loaded = true; }); } @@ -312,6 +320,39 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.onDayClicked.emit({day: day, month: this.month, year: this.year}); } + /** + * Check if user is viewing the current month. + */ + calculateIsCurrentMonth(): void { + const now = new Date(); + + this.isCurrentMonth = this.year == now.getFullYear() && this.month == now.getMonth() + 1; + } + + /** + * Go to current month. + */ + goToCurrentMonth(): void { + const now = new Date(), + initialMonth = this.month, + initialYear = this.year; + + this.month = now.getMonth() + 1; + this.year = now.getFullYear(); + + this.loaded = false; + + this.fetchEvents().then(() => { + this.isCurrentMonth = true; + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + this.year = initialYear; + this.month = initialMonth; + }).finally(() => { + this.loaded = true; + }); + } + /** * Decrease the current month. */ diff --git a/src/addon/calendar/pages/day/day.html b/src/addon/calendar/pages/day/day.html index e6db57d369f..0ef39ef7239 100644 --- a/src/addon/calendar/pages/day/day.html +++ b/src/addon/calendar/pages/day/day.html @@ -5,6 +5,9 @@ + diff --git a/src/addon/calendar/pages/day/day.ts b/src/addon/calendar/pages/day/day.ts index d18e25d75ea..a8c1e90a1bb 100644 --- a/src/addon/calendar/pages/day/day.ts +++ b/src/addon/calendar/pages/day/day.ts @@ -73,6 +73,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { hasOffline = false; isOnline = false; syncIcon: string; + isCurrentDay: boolean; constructor(localNotificationsProvider: CoreLocalNotificationsProvider, navParams: NavParams, @@ -196,7 +197,8 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { * View loaded. */ ngOnInit(): void { - this.currentMoment = moment().year(this.year).month(this.month - 1).date(this.day); + this.calculateCurrentMoment(); + this.calculateIsCurrentDay(); this.fetchData(true, false); } @@ -533,6 +535,52 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.navCtrl.push('AddonCalendarEditEventPage', params); } + /** + * Calculate current moment. + */ + calculateCurrentMoment(): void { + this.currentMoment = moment().year(this.year).month(this.month - 1).date(this.day); + } + + /** + * Check if user is viewing the current day. + */ + calculateIsCurrentDay(): void { + const now = new Date(); + + this.isCurrentDay = this.year == now.getFullYear() && this.month == now.getMonth() + 1 && this.day == now.getDate(); + } + + /** + * Go to current day. + */ + goToCurrentDay(): void { + const now = new Date(), + initialDay = this.day, + initialMonth = this.month, + initialYear = this.year; + + this.day = now.getDate(); + this.month = now.getMonth() + 1; + this.year = now.getFullYear(); + this.calculateCurrentMoment(); + + this.loaded = false; + + this.fetchEvents().then(() => { + this.isCurrentDay = true; + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + + this.year = initialYear; + this.month = initialMonth; + this.day = initialDay; + this.calculateCurrentMoment(); + }).finally(() => { + this.loaded = true; + }); + } + /** * Load next month. */ @@ -545,6 +593,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); this.decreaseDay(); }).finally(() => { + this.calculateIsCurrentDay(); this.loaded = true; }); } @@ -561,6 +610,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); this.increaseDay(); }).finally(() => { + this.calculateIsCurrentDay(); this.loaded = true; }); } diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html index ca8e832f3ad..adfbe89db1b 100644 --- a/src/addon/calendar/pages/index/index.html +++ b/src/addon/calendar/pages/index/index.html @@ -6,7 +6,7 @@ - - -
- + + + + + + + +
+ + + +
+
- - - -
- -
- - - -
-
+ + + +
+ +
+ + + +
+
- -
- - - - -
+ +
+ + + + +
- -
- - - - - - - + +
+ + + + + + + - -
- - - - - + +
+
+ + + + -
+ +
+
diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts index 8b419c87cc8..cf274423868 100644 --- a/src/core/course/components/format/format.ts +++ b/src/core/course/components/format/format.ts @@ -13,7 +13,7 @@ // limitations under the License. import { - Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Output, EventEmitter, ViewChildren, QueryList, Injector + Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Output, EventEmitter, ViewChildren, QueryList, Injector, ViewChild } from '@angular/core'; import { Content, ModalController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; @@ -24,6 +24,7 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreCourseFormatDelegate } from '@core/course/providers/format-delegate'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreBlockCourseBlocksComponent } from '@core/block/components/course-blocks/course-blocks'; import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; /** @@ -52,6 +53,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { @Output() completionChanged?: EventEmitter; // Will emit an event when any module completion changes. @ViewChildren(CoreDynamicComponent) dynamicComponents: QueryList; + @ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent: CoreBlockCourseBlocksComponent; // All the possible component classes. courseFormatComponent: any; @@ -420,6 +422,10 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { promises.push(Promise.resolve(component.callComponentFunction('doRefresh', [refresher, done, afterCompletionChange]))); }); + promises.push(this.courseBlocksComponent.invalidateBlocks().finally(() => { + return this.courseBlocksComponent.loadContent(); + })); + return Promise.all(promises); } diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index efbd0d7483a..d787aae14e3 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -20,6 +20,8 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreTabsComponent } from '@components/tabs/tabs'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCourseProvider } from '../../providers/course'; import { CoreCourseHelperProvider } from '../../providers/helper'; import { CoreCourseFormatDelegate } from '../../providers/format-delegate'; @@ -28,8 +30,6 @@ import { CoreCourseOptionsDelegate, CoreCourseOptionsHandlerToDisplay, CoreCourseOptionsMenuHandlerToDisplay } from '../../providers/options-delegate'; import { CoreCourseSyncProvider } from '../../providers/sync'; import { CoreCourseFormatComponent } from '../../components/format/format'; -import { CoreCoursesProvider } from '@core/courses/providers/courses'; -import { CoreTabsComponent } from '@components/tabs/tabs'; /** * Page that displays the list of courses the user is enrolled in. diff --git a/src/core/sitehome/components/index/core-sitehome-index.html b/src/core/sitehome/components/index/core-sitehome-index.html index 17a52f8b012..389a968accd 100644 --- a/src/core/sitehome/components/index/core-sitehome-index.html +++ b/src/core/sitehome/components/index/core-sitehome-index.html @@ -1,32 +1,30 @@ - + + + + + + + + + - - - - - - + + - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + diff --git a/src/core/sitehome/components/index/index.ts b/src/core/sitehome/components/index/index.ts index a629f76c46b..8c8a511c1ea 100644 --- a/src/core/sitehome/components/index/index.ts +++ b/src/core/sitehome/components/index/index.ts @@ -12,14 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, ViewChildren, QueryList, Input } from '@angular/core'; +import { Component, OnInit, Input, ViewChild } from '@angular/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; -import { CoreBlockDelegate } from '@core/block/providers/delegate'; -import { CoreBlockComponent } from '@core/block/components/block/block'; +import { CoreBlockCourseBlocksComponent } from '@core/block/components/course-blocks/course-blocks'; import { CoreSite } from '@classes/site'; /** @@ -30,21 +29,19 @@ import { CoreSite } from '@classes/site'; templateUrl: 'core-sitehome-index.html', }) export class CoreSiteHomeIndexComponent implements OnInit { - @ViewChildren(CoreBlockComponent) blocksComponents: QueryList; @Input() downloadEnabled: boolean; + @ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent: CoreBlockCourseBlocksComponent; dataLoaded = false; section: any; hasContent: boolean; - hasSupportedBlock: boolean; items: any[] = []; siteHomeId: number; currentSite: CoreSite; - blocks = []; constructor(private domUtils: CoreDomUtilsProvider, sitesProvider: CoreSitesProvider, private courseProvider: CoreCourseProvider, private courseHelper: CoreCourseHelperProvider, - private prefetchDelegate: CoreCourseModulePrefetchDelegate, private blockDelegate: CoreBlockDelegate) { + private prefetchDelegate: CoreCourseModulePrefetchDelegate) { this.currentSite = sitesProvider.getCurrentSite(); this.siteHomeId = this.currentSite.getSiteHomeId(); } @@ -79,19 +76,15 @@ export class CoreSiteHomeIndexComponent implements OnInit { promises.push(this.prefetchDelegate.invalidateModules(this.section.modules, this.siteHomeId)); } - if (this.courseProvider.canGetCourseBlocks()) { - promises.push(this.courseProvider.invalidateCourseBlocks(this.siteHomeId)); - } - - // Invalidate the blocks. - this.blocksComponents.forEach((blockComponent) => { - promises.push(blockComponent.invalidate().catch(() => { - // Ignore errors. - })); - }); + promises.push(this.courseBlocksComponent.invalidateBlocks()); Promise.all(promises).finally(() => { - this.loadContent().finally(() => { + const p2 = []; + + p2.push(this.loadContent()); + p2.push(this.courseBlocksComponent.loadContent()); + + return Promise.all(p2).finally(() => { refresher.complete(); }); }); @@ -149,32 +142,6 @@ export class CoreSiteHomeIndexComponent implements OnInit { this.currentSite && this.currentSite.getInfo().sitename).catch(() => { // Ignore errors. }); - - // Get site home blocks. - const canGetBlocks = this.courseProvider.canGetCourseBlocks(), - promise = canGetBlocks ? this.courseProvider.getCourseBlocks(this.siteHomeId) : Promise.reject(null); - - return promise.then((blocks) => { - this.blocks = blocks; - this.hasSupportedBlock = this.blockDelegate.hasSupportedBlock(blocks); - - }).catch((error) => { - if (canGetBlocks) { - this.domUtils.showErrorModal(error); - } - this.blocks = []; - - // Cannot get the blocks, just show site main menu if needed. - const section = sections.find((section) => section.section == 0); - if (section && this.courseHelper.sectionHasContent(section)) { - this.blocks.push({ - name: 'site_main_menu' - }); - this.hasSupportedBlock = true; - } else { - this.hasSupportedBlock = false; - } - }); }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true); }); From 90f340aaa5a0cde3de27b874f142f66bed935149 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 26 Jul 2019 15:48:25 +0200 Subject: [PATCH 130/241] MOBILE-3074 format-text: Fix size calculation when content has images --- src/directives/external-content.ts | 13 +++- src/directives/format-text.ts | 104 ++++++++++++++++++----------- 2 files changed, 76 insertions(+), 41 deletions(-) diff --git a/src/directives/external-content.ts b/src/directives/external-content.ts index 019cd4657c8..68937d89956 100644 --- a/src/directives/external-content.ts +++ b/src/directives/external-content.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Directive, Input, AfterViewInit, ElementRef, OnChanges, SimpleChange } from '@angular/core'; +import { Directive, Input, AfterViewInit, ElementRef, OnChanges, SimpleChange, Output, EventEmitter } from '@angular/core'; import { Platform } from 'ionic-angular'; import { CoreAppProvider } from '@providers/app'; import { CoreLoggerProvider } from '@providers/logger'; @@ -43,6 +43,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges { @Input() href?: string; @Input('target-src') targetSrc?: string; @Input() poster?: string; + @Output() onLoad = new EventEmitter(); // Emitted when content is loaded. Only for images. protected element: HTMLElement; protected logger; @@ -225,7 +226,17 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges { // The browser does not catch changes in SRC, we need to add a new source. this.addSource(finalUrl); } else { + if (tagName === 'IMG') { + const listener = (): void => { + this.element.removeEventListener('load', listener); + this.element.removeEventListener('error', listener); + this.onLoad.emit(); + }; + this.element.addEventListener('load', listener); + this.element.addEventListener('error', listener); + } this.element.setAttribute(targetAttr, finalUrl); + this.element.setAttribute('data-original-' + targetAttr, url); } // Set events to download big files (not downloaded automatically). diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index 489561674e9..7cba685c792 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -90,8 +90,9 @@ export class CoreFormatTextDirective implements OnChanges { * Apply CoreExternalContentDirective to a certain element. * * @param {HTMLElement} element Element to add the attributes to. + * @return {CoreExternalContentDirective} External content instance. */ - protected addExternalContent(element: HTMLElement): void { + protected addExternalContent(element: HTMLElement): CoreExternalContentDirective { // Angular 2 doesn't let adding directives dynamically. Create the CoreExternalContentDirective manually. const extContent = new CoreExternalContentDirective( element, this.loggerProvider, this.filepoolProvider, this.platform, this.sitesProvider, this.domUtils, this.urlUtils, this.appProvider, this.utils); @@ -105,6 +106,8 @@ export class CoreFormatTextDirective implements OnChanges { extContent.poster = element.getAttribute('poster'); extContent.ngAfterViewInit(); + + return extContent; } /** @@ -117,15 +120,13 @@ export class CoreFormatTextDirective implements OnChanges { } /** - * Wrap an image with a container to adapt its width and, if needed, add an anchor to view it in full size. + * Wrap an image with a container to adapt its width. * - * @param {number} elWidth Width of the directive's element. * @param {HTMLElement} img Image to adapt. */ - protected adaptImage(elWidth: number, img: HTMLElement): void { - const imgWidth = this.getElementWidth(img), - // Element to wrap the image. - container = document.createElement('span'), + protected adaptImage(img: HTMLElement): void { + // Element to wrap the image. + const container = document.createElement('span'), originalWidth = img.attributes.getNamedItem('width'); const forcedWidth = parseInt(originalWidth && originalWidth.value); @@ -152,36 +153,48 @@ export class CoreFormatTextDirective implements OnChanges { } this.domUtils.wrapElement(img, container); - - if (imgWidth > elWidth) { - // The image has been adapted, add an anchor to view it in full size. - this.addMagnifyingGlass(container, img); - } } /** - * Add a magnifying glass icon to view an image at full size. - * - * @param {HTMLElement} container The container of the image. - * @param {HTMLElement} img The image. + * Add magnifying glass icons to view adapted images at full size. */ - addMagnifyingGlass(container: HTMLElement, img: HTMLElement): void { - const imgSrc = this.textUtils.escapeHTML(img.getAttribute('src')), + addMagnifyingGlasses(): void { + const imgs = Array.from(this.element.querySelectorAll('.core-adapted-img-container > img')); + if (!imgs.length) { + return; + } + + // If cannot calculate element's width, use viewport width to avoid false adapt image icons appearing. + const elWidth = this.getElementWidth(this.element) || window.innerWidth; + + imgs.forEach((img: HTMLImageElement) => { + let imgWidth = parseInt(img.getAttribute('width')); + if (!imgWidth) { + // No width attribute, use real size. + imgWidth = img.naturalWidth; + } + + if (imgWidth <= elWidth) { + return; + } + + const imgSrc = this.textUtils.escapeHTML(img.getAttribute('data-original-src') || img.getAttribute('src')), label = this.textUtils.escapeHTML(this.translate.instant('core.openfullimage')), anchor = document.createElement('a'); - anchor.classList.add('core-image-viewer-icon'); - anchor.setAttribute('aria-label', label); - // Add an ion-icon item to apply the right styles, but the ion-icon component won't be executed. - anchor.innerHTML = ''; + anchor.classList.add('core-image-viewer-icon'); + anchor.setAttribute('aria-label', label); + // Add an ion-icon item to apply the right styles, but the ion-icon component won't be executed. + anchor.innerHTML = ''; - anchor.addEventListener('click', (e: Event) => { - e.preventDefault(); - e.stopPropagation(); - this.domUtils.viewImage(imgSrc, img.getAttribute('alt'), this.component, this.componentId); - }); + anchor.addEventListener('click', (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + this.domUtils.viewImage(imgSrc, img.getAttribute('alt'), this.component, this.componentId); + }); - container.appendChild(anchor); + img.parentNode.appendChild(anchor); + }); } /** @@ -307,12 +320,8 @@ export class CoreFormatTextDirective implements OnChanges { // Calculate the height now. this.calculateHeight(); - // Wait for images to load and calculate the height again if needed. - this.domUtils.waitForImages(this.element).then((hasImgToLoad) => { - if (hasImgToLoad) { - this.calculateHeight(); - } - }); + // Add magnifying glasses to images. + this.addMagnifyingGlasses(); if (!this.loadingChangedListener) { // Recalculate the height if a parent core-loading displays the content. @@ -387,16 +396,14 @@ export class CoreFormatTextDirective implements OnChanges { this.addExternalContent(anchor); }); + const externalImages: CoreExternalContentDirective[] = []; if (images && images.length > 0) { - // If cannot calculate element's width, use a medium number to avoid false adapt image icons appearing. - const elWidth = this.getElementWidth(this.element) || 100; - // Walk through the content to find images, and add our directive. images.forEach((img: HTMLElement) => { this.addMediaAdaptClass(img); - this.addExternalContent(img); + externalImages.push(this.addExternalContent(img)); if (this.utils.isTrueOrOne(this.adaptImg) && !img.classList.contains('icon')) { - this.adaptImage(elWidth, img); + this.adaptImage(img); } }); } @@ -445,7 +452,24 @@ export class CoreFormatTextDirective implements OnChanges { this.domUtils.handleBootstrapTooltips(div); - return div; + // Wait for images to load. + let promise: Promise = null; + if (externalImages.length) { + promise = Promise.all(externalImages.map((externalImage) => { + return new Promise((resolve): void => { + const subscription = externalImage.onLoad.subscribe(() => { + subscription.unsubscribe(); + resolve(); + }); + }); + })); + } else { + promise = Promise.resolve(); + } + + return promise.then(() => { + return div; + }); }); } From 7507e9306847e6b9c688e4be6611b721fc7e969b Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 29 Jul 2019 10:16:10 +0200 Subject: [PATCH 131/241] MOBILE-3074 format-text: No magnifying glasses for images inside links --- src/directives/format-text.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index 7cba685c792..2f851c36161 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -168,6 +168,11 @@ export class CoreFormatTextDirective implements OnChanges { const elWidth = this.getElementWidth(this.element) || window.innerWidth; imgs.forEach((img: HTMLImageElement) => { + // Skip image if it's inside a link. + if (img.closest('a')) { + return; + } + let imgWidth = parseInt(img.getAttribute('width')); if (!imgWidth) { // No width attribute, use real size. From a6368a5bbdcc5d1702640962a127c5ed1440d484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 29 Jul 2019 13:26:05 +0200 Subject: [PATCH 132/241] MOBILE-3062 module: Change module tag to ion-item --- src/addon/mod/label/label.scss | 8 ++++---- .../course/components/module/core-course-module.html | 4 ++-- src/core/course/components/module/module.scss | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/addon/mod/label/label.scss b/src/addon/mod/label/label.scss index 9f6514ba6cc..46d2a860030 100644 --- a/src/addon/mod/label/label.scss +++ b/src/addon/mod/label/label.scss @@ -1,4 +1,4 @@ -a.core-course-module-handler.addon-mod-label-handler { +.item.core-course-module-handler.addon-mod-label-handler { align-items: center; &:hover { @@ -6,14 +6,14 @@ a.core-course-module-handler.addon-mod-label-handler { } } -.md a.core-course-module-handler.addon-mod-label-handler .item-inner { +.md .item.core-course-module-handler.addon-mod-label-handler .item-inner { padding-bottom: $item-md-padding-bottom; } -.ios a.core-course-module-handler.addon-mod-label-handler .item-inner { +.ios .item.core-course-module-handler.addon-mod-label-handler .item-inner { padding-bottom: $item-ios-padding-bottom; } -.wp a.core-course-module-handler.addon-mod-label-handler .item-inner { +.wp .item.core-course-module-handler.addon-mod-label-handler .item-inner { padding-bottom: $item-wp-padding-bottom; } diff --git a/src/core/course/components/module/core-course-module.html b/src/core/course/components/module/core-course-module.html index 55686efd032..eae6da52c4f 100644 --- a/src/core/course/components/module/core-course-module.html +++ b/src/core/course/components/module/core-course-module.html @@ -1,4 +1,4 @@ -
+
@@ -32,4 +32,4 @@ {{ 'core.course.manualcompletionnotsynced' | translate }}
-
\ No newline at end of file + \ No newline at end of file diff --git a/src/core/course/components/module/module.scss b/src/core/course/components/module/module.scss index 410482e7639..c08daa491ca 100644 --- a/src/core/course/components/module/module.scss +++ b/src/core/course/components/module/module.scss @@ -2,7 +2,7 @@ ion-app.app-root core-course-module { background: white; display: block; - a.core-course-module-handler { + .item.core-course-module-handler { align-items: flex-start; min-height: 52px; @@ -80,7 +80,7 @@ ion-app.app-root.md core-course-module { } } - a.core-course-module-handler .core-module-icon { + .item.core-course-module-handler .core-module-icon { margin-top: $label-md-margin-top; margin-bottom: $label-md-margin-bottom; width: 24px; @@ -110,7 +110,7 @@ ion-app.app-root.ios core-course-module { } } - a.core-course-module-handler .core-module-icon { + .item.core-course-module-handler .core-module-icon { margin-top: $label-ios-margin-top; margin-bottom: $label-ios-margin-bottom; width: 24px; @@ -137,7 +137,7 @@ ion-app.app-root.wp core-course-module { } } - a.core-course-module-handler .core-module-icon { + .item.core-course-module-handler .core-module-icon { margin-top: $item-wp-padding-top; margin-bottom: $item-wp-padding-bottom; width: 24px; @@ -154,6 +154,6 @@ ion-app.app-root.wp core-course-module { } } -ion-app.app-root a.core-course-module-handler.item [item-start] + .item-inner { +ion-app.app-root .core-course-module-handler.item [item-start] + .item-inner { @include margin-horizontal(4px, null); } \ No newline at end of file From a927a5619ed9094d2e5ca9f35dff1b3497b468c0 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 29 Jul 2019 10:18:06 +0200 Subject: [PATCH 133/241] MOBILE-3104 calendar: Store events in local DB with new WS --- src/addon/calendar/pages/event/event.ts | 4 +- src/addon/calendar/providers/calendar.ts | 235 ++++++++++++++++++----- 2 files changed, 186 insertions(+), 53 deletions(-) diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index fb935474828..5789fb5745b 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -150,7 +150,7 @@ export class AddonCalendarEventPage implements OnDestroy { */ fetchEvent(sync?: boolean, showErrors?: boolean): Promise { const currentSite = this.sitesProvider.getCurrentSite(), - canGetById = this.calendarProvider.isGetEventByIdAvailable(); + canGetById = this.calendarProvider.isGetEventByIdAvailableInSite(); let promise, deleted = false; @@ -278,7 +278,7 @@ export class AddonCalendarEventPage implements OnDestroy { })); } - if (canGetById && event.iscategoryevent) { + if (canGetById && event.iscategoryevent && event.category) { this.categoryPath = event.category.nestedname; } diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index e7c0800f3c1..eea082da324 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -91,11 +91,12 @@ export class AddonCalendarProvider { ]; // Variables for database. - static EVENTS_TABLE = 'addon_calendar_events_2'; + static EVENTS_TABLE = 'addon_calendar_events_3'; static REMINDERS_TABLE = 'addon_calendar_reminders'; protected siteSchema: CoreSiteSchema = { name: 'AddonCalendarProvider', - version: 2, + version: 3, + canBeCleared: [ AddonCalendarProvider.EVENTS_TABLE ], tables: [ { name: AddonCalendarProvider.EVENTS_TABLE, @@ -177,6 +178,82 @@ export class AddonCalendarProvider { { name: 'subscriptionid', type: 'INTEGER' + }, + { + name: 'location', + type: 'TEXT' + }, + { + name: 'eventcount', + type: 'INTEGER' + }, + { + name: 'timesort', + type: 'INTEGER' + }, + { + name: 'category', + type: 'TEXT' + }, + { + name: 'course', + type: 'TEXT' + }, + { + name: 'subscription', + type: 'TEXT' + }, + { + name: 'canedit', + type: 'INTEGER' + }, + { + name: 'candelete', + type: 'INTEGER' + }, + { + name: 'deleteurl', + type: 'TEXT' + }, + { + name: 'editurl', + type: 'TEXT' + }, + { + name: 'viewurl', + type: 'TEXT' + }, + { + name: 'formattedtime', + type: 'TEXT' + }, + { + name: 'isactionevent', + type: 'INTEGER' + }, + { + name: 'url', + type: 'TEXT' + }, + { + name: 'islastday', + type: 'INTEGER' + }, + { + name: 'popupname', + type: 'TEXT' + }, + { + name: 'mindaytimestamp', + type: 'INTEGER' + }, + { + name: 'maxdaytimestamp', + type: 'INTEGER' + }, + { + name: 'draggable', + type: 'INTEGER' } ] }, @@ -203,56 +280,34 @@ export class AddonCalendarProvider { } ], migrate(db: SQLiteDB, oldVersion: number, siteId: string): Promise | void { - if (oldVersion < 2) { + if (oldVersion < 3) { const newTable = AddonCalendarProvider.EVENTS_TABLE; - const oldTable = 'addon_calendar_events'; - - return db.tableExists(oldTable).then(() => { - return db.getAllRecords(oldTable).then((events) => { - const now = Math.round(Date.now() / 1000); - - return Promise.all(events.map((event) => { - if (event.notificationtime == 0) { - // No reminders. - return Promise.resolve(); - } - - let time; + let oldTable = 'addon_calendar_events_2'; - if (event.notificationtime == -1) { - time = -1; - } else { - time = event.timestart - event.notificationtime * 60; + return db.tableExists(oldTable).catch(() => { + // The v2 table doesn't exist, try with v1. + oldTable = 'addon_calendar_events'; - if (time < now) { - // Old reminder, just not add this. - return Promise.resolve(); - } - } + return db.tableExists(oldTable); + }).then(() => { + // Move the records from the old table. + // Move the records from the old table. + return db.getAllRecords(oldTable).then((events) => { + const promises = []; - const reminder = { - eventid: event.id, - time: time - }; - - // Cancel old notification. - this.localNotificationsProvider.cancel(event.id, AddonCalendarProvider.COMPONENT, siteId); - - return db.insertRecord(AddonCalendarProvider.REMINDERS_TABLE, reminder); - })).then(() => { - // Move the records from the old table. - return db.insertRecordsFrom(newTable, oldTable, undefined, 'id, name, description, format, eventtype,\ - courseid, timestart, timeduration, categoryid, groupid, userid, instance, modulename, timemodified,\ - repeatid, visible, uuid, sequence, subscriptionid'); - }).then(() => { - return db.dropTable(oldTable); + events.forEach((event) => { + promises.push(db.insertRecord(newTable, event)); }); + + return Promise.all(promises); }); + }).then(() => { + return db.dropTable(oldTable); }).catch(() => { // Old table does not exist, ignore. }); } - } + }, }; protected logger; @@ -369,6 +424,11 @@ export class AddonCalendarProvider { */ cleanExpiredEvents(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { + if (this.canViewMonthInSite(site)) { + // Site supports monthly view, don't clean expired events because user can see past events. + return; + } + return site.getDb().getRecordsSelect(AddonCalendarProvider.EVENTS_TABLE, 'timestart + timeduration < ?', [this.timeUtils.timestamp()]).then((events) => { return Promise.all(events.map((event) => { @@ -805,6 +865,10 @@ export class AddonCalendarProvider { return site.read('core_calendar_get_calendar_event_by_id', data, preSets).then((response) => { return response.event; + }).catch((error) => { + return this.getEventFromLocalDb(id).catch(() => { + return Promise.reject(error); + }); }); }); } @@ -828,7 +892,20 @@ export class AddonCalendarProvider { */ getEventFromLocalDb(id: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getRecord(AddonCalendarProvider.EVENTS_TABLE, { id: id }); + return site.getDb().getRecord(AddonCalendarProvider.EVENTS_TABLE, { id: id }).then((event) => { + if (this.isGetEventByIdAvailableInSite(site)) { + // Calculate data to match the new WS. + event.descriptionformat = event.format; + event.iscourseevent = event.eventtype == AddonCalendarProvider.TYPE_COURSE; + event.iscategoryevent = event.eventtype == AddonCalendarProvider.TYPE_CATEGORY; + event.normalisedeventtype = this.getEventType(event); + event.category = this.textUtils.parseJSON(event.category, null); + event.course = this.textUtils.parseJSON(event.course, null); + event.subscription = this.textUtils.parseJSON(event.subscription, null); + } + + return event; + }); }); } @@ -918,7 +995,11 @@ export class AddonCalendarProvider { updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; - return site.read('core_calendar_get_calendar_day_view', data, preSets); + return site.read('core_calendar_get_calendar_day_view', data, preSets).then((response) => { + this.storeEventsInLocalDB(response.events, siteId); + + return response.events; + }); }); } @@ -1034,7 +1115,10 @@ export class AddonCalendarProvider { }; return site.read('core_calendar_get_calendar_events', data, preSets).then((response) => { - this.storeEventsInLocalDB(response.events, siteId); + if (!this.canViewMonthInSite(site)) { + // Store events only in 3.1-3.3. In 3.4+ we'll use the new WS that return more info. + this.storeEventsInLocalDB(response.events, siteId); + } return response.events; }); @@ -1094,7 +1178,15 @@ export class AddonCalendarProvider { updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; - return site.read('core_calendar_get_calendar_monthly_view', data, preSets); + return site.read('core_calendar_get_calendar_monthly_view', data, preSets).then((response) => { + response.weeks.forEach((week) => { + week.days.forEach((day) => { + this.storeEventsInLocalDB(day.events, siteId); + }); + }); + + return response; + }); }); } @@ -1158,7 +1250,11 @@ export class AddonCalendarProvider { updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; - return site.read('core_calendar_get_calendar_upcoming_view', data, preSets); + return site.read('core_calendar_get_calendar_upcoming_view', data, preSets).then((response) => { + this.storeEventsInLocalDB(response.events, siteId); + + return response; + }); }); } @@ -1402,11 +1498,29 @@ export class AddonCalendarProvider { /** * Check if the get event by ID WS is available. * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if available. + * @since 3.4 + */ + isGetEventByIdAvailable(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.isGetEventByIdAvailableInSite(site); + }).catch(() => { + return false; + }); + } + + /** + * Check if the get event by ID WS is available in a certain site. + * + * @param {CoreSite} [site] Site. If not defined, use current site. * @return {boolean} Whether it's available. * @since 3.4 */ - isGetEventByIdAvailable(): boolean { - return this.sitesProvider.wsAvailableInCurrentSite('core_calendar_get_calendar_event_by_id'); + isGetEventByIdAvailableInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.wsAvailable('core_calendar_get_calendar_event_by_id'); } /** @@ -1574,11 +1688,12 @@ export class AddonCalendarProvider { } }); }).then(() => { + // Don't store data that can be calculated like formattedtime, iscategoryevent, etc. const eventRecord = { id: event.id, name: event.name, description: event.description, - format: event.format, + format: event.descriptionformat || event.format, eventtype: event.eventtype, courseid: event.courseid, timestart: event.timestart, @@ -1593,7 +1708,25 @@ export class AddonCalendarProvider { visible: event.visible, uuid: event.uuid, sequence: event.sequence, - subscriptionid: event.subscriptionid + subscriptionid: event.subscriptionid, + location: event.location, + eventcount: event.eventcount, + timesort: event.timesort, + category: event.category ? JSON.stringify(event.category) : undefined, + course: event.course ? JSON.stringify(event.course) : undefined, + subscription: event.subscription ? JSON.stringify(event.subscription) : undefined, + canedit: event.canedit ? 1 : 0, + candelete: event.candelete ? 1 : 0, + deleteurl: event.deleteurl, + editurl: event.editurl, + viewurl: event.viewurl, + isactionevent: event.isactionevent ? 1 : 0, + url: event.url, + islastday: event.islastday ? 1 : 0, + popupname: event.popupname, + mindaytimestamp: event.mindaytimestamp, + maxdaytimestamp: event.maxdaytimestamp, + draggable: event.draggable, }; return site.getDb().insertRecord(AddonCalendarProvider.EVENTS_TABLE, eventRecord); From f9fb7d546840ea67e34fc91644302282d092b771 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 29 Jul 2019 16:10:54 +0200 Subject: [PATCH 134/241] MOBILE-3044 lesson: Fix PTR in Reports tab --- .../lesson/components/index/addon-mod-lesson-index.html | 2 +- src/addon/mod/lesson/components/index/index.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html b/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html index 55cc163c85d..655de63019e 100644 --- a/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html +++ b/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html @@ -16,7 +16,7 @@ - + diff --git a/src/addon/mod/lesson/components/index/index.ts b/src/addon/mod/lesson/components/index/index.ts index 8a0acdba1c3..9c3458b72b2 100644 --- a/src/addon/mod/lesson/components/index/index.ts +++ b/src/addon/mod/lesson/components/index/index.ts @@ -384,10 +384,19 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo }); } + /** + * First tab selected. + */ + indexSelected(): void { + this.selectedTab = 0; + } + /** * Reports tab selected. */ reportsSelected(): void { + this.selectedTab = 1; + if (!this.groupInfo) { this.fetchReportData().catch((error) => { this.domUtils.showErrorModalDefault(error, 'Error getting report.'); From 627e25593a6c985047fc5292d2711b9b427893c9 Mon Sep 17 00:00:00 2001 From: Mark Johnson Date: Mon, 1 Jul 2019 11:58:19 +0100 Subject: [PATCH 135/241] MOBILE-3092 dashboard: Disable empty options in course selector --- .../myoverview/addon-block-myoverview.html | 10 +++++----- .../components/myoverview/myoverview.ts | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/addon/block/myoverview/components/myoverview/addon-block-myoverview.html b/src/addon/block/myoverview/components/myoverview/addon-block-myoverview.html index 598c204f0ed..294a499c44a 100644 --- a/src/addon/block/myoverview/components/myoverview/addon-block-myoverview.html +++ b/src/addon/block/myoverview/components/myoverview/addon-block-myoverview.html @@ -19,11 +19,11 @@

{{ 'addon.block_myoverview.pluginname' | translate }}

{{ 'addon.block_myoverview.all' | translate }}∫ - {{ 'addon.block_myoverview.inprogress' | translate }} - {{ 'addon.block_myoverview.future' | translate }} - {{ 'addon.block_myoverview.past' | translate }} - {{ 'addon.block_myoverview.favourites' | translate }} - {{ 'addon.block_myoverview.hiddencourses' | translate }} + {{ 'addon.block_myoverview.inprogress' | translate }} + {{ 'addon.block_myoverview.future' | translate }} + {{ 'addon.block_myoverview.past' | translate }} + {{ 'addon.block_myoverview.favourites' | translate }} + {{ 'addon.block_myoverview.hiddencourses' | translate }}
diff --git a/src/addon/block/myoverview/components/myoverview/myoverview.ts b/src/addon/block/myoverview/components/myoverview/myoverview.ts index b36202f7e09..4848e4d5c8f 100644 --- a/src/addon/block/myoverview/components/myoverview/myoverview.ts +++ b/src/addon/block/myoverview/components/myoverview/myoverview.ts @@ -64,6 +64,11 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem showSortFilter = false; downloadCourseEnabled: boolean; downloadCoursesEnabled: boolean; + disableInProgress = false; + disablePast = false; + disableFuture = false; + disableFavourite = false; + disableHidden = false; protected prefetchIconsInitialized = false; protected isDestroyed; @@ -173,12 +178,17 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem this.courses.filter = ''; this.showFilter = false; + this.disableInProgress = this.courses.inprogress.length === 0; + this.disablePast = this.courses.past.length === 0; + this.disableFuture = this.courses.future.length === 0; this.showSelectorFilter = courses.length > 0 && (this.courses.past.length > 0 || this.courses.future.length > 0 || - typeof courses[0].enddate != 'undefined'); + typeof courses[0].enddate != 'undefined'); this.showHidden = this.showSelectorFilter && typeof courses[0].hidden != 'undefined'; + this.disableHidden = this.courses.hidden.length === 0; this.showFavourite = this.showSelectorFilter && typeof courses[0].isfavourite != 'undefined'; - if (!this.showSelectorFilter) { - // No selector, show all. + this.disableFavourite = this.courses.favourite.length === 0; + if (!this.showSelectorFilter || (this.selectedFilter === 'inprogress' && this.disableInProgress)) { + // No selector, or the default option is disabled, show all. this.selectedFilter = 'all'; } this.filteredCourses = this.courses[this.selectedFilter]; From 767c0c19d4feab61c134f4e467b1bf604794eedc Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 30 Jul 2019 16:16:45 +0200 Subject: [PATCH 136/241] MOBILE-3108 siteplugins: Allow create only title and prerendered blocks --- .../block/badges/providers/block-handler.ts | 5 +- .../block/blogmenu/providers/block-handler.ts | 5 +- .../blogrecent/providers/block-handler.ts | 5 +- .../block/blogtags/providers/block-handler.ts | 5 +- .../glossaryrandom/providers/block-handler.ts | 5 +- .../newsitems/providers/block-handler.ts | 5 +- .../onlineusers/providers/block-handler.ts | 5 +- .../recentactivity/providers/block-handler.ts | 5 +- .../rssclient/providers/block-handler.ts | 5 +- .../block/tags/providers/block-handler.ts | 5 +- .../core-block-pre-rendered.html | 2 +- .../classes/handlers/block-handler.ts | 27 +++++--- .../siteplugins/components/block/block.ts | 5 +- .../components/components.module.ts | 4 ++ .../core-siteplugins-only-title-block.html | 3 + .../only-title-block/only-title-block.ts | 69 +++++++++++++++++++ src/core/siteplugins/providers/helper.ts | 15 ++-- 17 files changed, 126 insertions(+), 49 deletions(-) create mode 100644 src/core/siteplugins/components/only-title-block/core-siteplugins-only-title-block.html create mode 100644 src/core/siteplugins/components/only-title-block/only-title-block.ts diff --git a/src/addon/block/badges/providers/block-handler.ts b/src/addon/block/badges/providers/block-handler.ts index 9cf9b36227e..d6cfb677a39 100644 --- a/src/addon/block/badges/providers/block-handler.ts +++ b/src/addon/block/badges/providers/block-handler.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { CoreBlockHandlerData } from '@core/block/providers/delegate'; import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; @@ -27,7 +26,7 @@ export class AddonBlockBadgesHandler extends CoreBlockBaseHandler { name = 'AddonBlockBadges'; blockName = 'badges'; - constructor(private translate: TranslateService) { + constructor() { super(); } @@ -44,7 +43,7 @@ export class AddonBlockBadgesHandler extends CoreBlockBaseHandler { : CoreBlockHandlerData | Promise { return { - title: this.translate.instant('addon.block_badges.pluginname'), + title: 'addon.block_badges.pluginname', class: 'addon-block-badges', component: CoreBlockPreRenderedComponent }; diff --git a/src/addon/block/blogmenu/providers/block-handler.ts b/src/addon/block/blogmenu/providers/block-handler.ts index 362b978994a..231137b8e0f 100644 --- a/src/addon/block/blogmenu/providers/block-handler.ts +++ b/src/addon/block/blogmenu/providers/block-handler.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { CoreBlockHandlerData } from '@core/block/providers/delegate'; import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; @@ -27,7 +26,7 @@ export class AddonBlockBlogMenuHandler extends CoreBlockBaseHandler { name = 'AddonBlockBlogMenu'; blockName = 'blog_menu'; - constructor(private translate: TranslateService) { + constructor() { super(); } @@ -44,7 +43,7 @@ export class AddonBlockBlogMenuHandler extends CoreBlockBaseHandler { : CoreBlockHandlerData | Promise { return { - title: this.translate.instant('addon.block_blogmenu.pluginname'), + title: 'addon.block_blogmenu.pluginname', class: 'addon-block-blog-menu', component: CoreBlockPreRenderedComponent }; diff --git a/src/addon/block/blogrecent/providers/block-handler.ts b/src/addon/block/blogrecent/providers/block-handler.ts index 69140e96dc1..55f03cb8ed7 100644 --- a/src/addon/block/blogrecent/providers/block-handler.ts +++ b/src/addon/block/blogrecent/providers/block-handler.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { CoreBlockHandlerData } from '@core/block/providers/delegate'; import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; @@ -27,7 +26,7 @@ export class AddonBlockBlogRecentHandler extends CoreBlockBaseHandler { name = 'AddonBlockBlogRecent'; blockName = 'blog_recent'; - constructor(private translate: TranslateService) { + constructor() { super(); } @@ -44,7 +43,7 @@ export class AddonBlockBlogRecentHandler extends CoreBlockBaseHandler { : CoreBlockHandlerData | Promise { return { - title: this.translate.instant('addon.block_blogrecent.pluginname'), + title: 'addon.block_blogrecent.pluginname', class: 'addon-block-blog-recent', component: CoreBlockPreRenderedComponent }; diff --git a/src/addon/block/blogtags/providers/block-handler.ts b/src/addon/block/blogtags/providers/block-handler.ts index cd6ae4c4677..aa2a17495c9 100644 --- a/src/addon/block/blogtags/providers/block-handler.ts +++ b/src/addon/block/blogtags/providers/block-handler.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { CoreBlockHandlerData } from '@core/block/providers/delegate'; import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; @@ -27,7 +26,7 @@ export class AddonBlockBlogTagsHandler extends CoreBlockBaseHandler { name = 'AddonBlockBlogTags'; blockName = 'blog_tags'; - constructor(private translate: TranslateService) { + constructor() { super(); } @@ -44,7 +43,7 @@ export class AddonBlockBlogTagsHandler extends CoreBlockBaseHandler { : CoreBlockHandlerData | Promise { return { - title: this.translate.instant('addon.block_blogtags.pluginname'), + title: 'addon.block_blogtags.pluginname', class: 'addon-block-blog-tags', component: CoreBlockPreRenderedComponent }; diff --git a/src/addon/block/glossaryrandom/providers/block-handler.ts b/src/addon/block/glossaryrandom/providers/block-handler.ts index 48b97b5a1b3..d639ccb16fa 100644 --- a/src/addon/block/glossaryrandom/providers/block-handler.ts +++ b/src/addon/block/glossaryrandom/providers/block-handler.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { CoreBlockHandlerData } from '@core/block/providers/delegate'; import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; @@ -26,7 +25,7 @@ export class AddonBlockGlossaryRandomHandler extends CoreBlockBaseHandler { name = 'AddonBlockGlossaryRandom'; blockName = 'glossary_random'; - constructor(private translate: TranslateService) { + constructor() { super(); } @@ -42,7 +41,7 @@ export class AddonBlockGlossaryRandomHandler extends CoreBlockBaseHandler { getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) : CoreBlockHandlerData | Promise { return { - title: block.contents.title || this.translate.instant('addon.block_glossaryrandom.pluginname'), + title: block.contents.title || 'addon.block_glossaryrandom.pluginname', class: 'addon-block-glossary-random', component: CoreBlockPreRenderedComponent }; diff --git a/src/addon/block/newsitems/providers/block-handler.ts b/src/addon/block/newsitems/providers/block-handler.ts index 9b6c15cb831..c077474d8a0 100644 --- a/src/addon/block/newsitems/providers/block-handler.ts +++ b/src/addon/block/newsitems/providers/block-handler.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { CoreBlockHandlerData } from '@core/block/providers/delegate'; import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; @@ -26,7 +25,7 @@ export class AddonBlockNewsItemsHandler extends CoreBlockBaseHandler { name = 'AddonBlockNewsItems'; blockName = 'news_items'; - constructor(private translate: TranslateService) { + constructor() { super(); } @@ -42,7 +41,7 @@ export class AddonBlockNewsItemsHandler extends CoreBlockBaseHandler { getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) : CoreBlockHandlerData | Promise { return { - title: this.translate.instant('addon.block_newsitems.pluginname'), + title: 'addon.block_newsitems.pluginname', class: 'addon-block-news-items', component: CoreBlockPreRenderedComponent }; diff --git a/src/addon/block/onlineusers/providers/block-handler.ts b/src/addon/block/onlineusers/providers/block-handler.ts index 9967ad6f679..353835c830c 100644 --- a/src/addon/block/onlineusers/providers/block-handler.ts +++ b/src/addon/block/onlineusers/providers/block-handler.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { CoreBlockHandlerData } from '@core/block/providers/delegate'; import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; @@ -26,7 +25,7 @@ export class AddonBlockOnlineUsersHandler extends CoreBlockBaseHandler { name = 'AddonBlockOnlineUsers'; blockName = 'online_users'; - constructor(private translate: TranslateService) { + constructor() { super(); } @@ -42,7 +41,7 @@ export class AddonBlockOnlineUsersHandler extends CoreBlockBaseHandler { getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) : CoreBlockHandlerData | Promise { return { - title: this.translate.instant('addon.block_onlineusers.pluginname'), + title: 'addon.block_onlineusers.pluginname', class: 'addon-block-online-users', component: CoreBlockPreRenderedComponent }; diff --git a/src/addon/block/recentactivity/providers/block-handler.ts b/src/addon/block/recentactivity/providers/block-handler.ts index 043acd495a6..ac69af02b3f 100644 --- a/src/addon/block/recentactivity/providers/block-handler.ts +++ b/src/addon/block/recentactivity/providers/block-handler.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { CoreBlockHandlerData } from '@core/block/providers/delegate'; import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; @@ -27,7 +26,7 @@ export class AddonBlockRecentActivityHandler extends CoreBlockBaseHandler { name = 'AddonBlockRecentActivity'; blockName = 'recent_activity'; - constructor(private translate: TranslateService) { + constructor() { super(); } @@ -44,7 +43,7 @@ export class AddonBlockRecentActivityHandler extends CoreBlockBaseHandler { : CoreBlockHandlerData | Promise { return { - title: this.translate.instant('addon.block_recentactivity.pluginname'), + title: 'addon.block_recentactivity.pluginname', class: 'addon-block-recent-activity', component: CoreBlockPreRenderedComponent }; diff --git a/src/addon/block/rssclient/providers/block-handler.ts b/src/addon/block/rssclient/providers/block-handler.ts index ce26caba41e..8976f218250 100644 --- a/src/addon/block/rssclient/providers/block-handler.ts +++ b/src/addon/block/rssclient/providers/block-handler.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { CoreBlockHandlerData } from '@core/block/providers/delegate'; import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; @@ -27,7 +26,7 @@ export class AddonBlockRssClientHandler extends CoreBlockBaseHandler { name = 'AddonBlockRssClient'; blockName = 'rss_client'; - constructor(private translate: TranslateService) { + constructor() { super(); } @@ -44,7 +43,7 @@ export class AddonBlockRssClientHandler extends CoreBlockBaseHandler { : CoreBlockHandlerData | Promise { return { - title: block.contents.title || this.translate.instant('addon.block_rssclient.pluginname'), + title: block.contents.title || 'addon.block_rssclient.pluginname', class: 'addon-block-rss-client', component: CoreBlockPreRenderedComponent }; diff --git a/src/addon/block/tags/providers/block-handler.ts b/src/addon/block/tags/providers/block-handler.ts index 749188709dd..f1c7d4cd83e 100644 --- a/src/addon/block/tags/providers/block-handler.ts +++ b/src/addon/block/tags/providers/block-handler.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { CoreBlockHandlerData } from '@core/block/providers/delegate'; import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; @@ -27,7 +26,7 @@ export class AddonBlockTagsHandler extends CoreBlockBaseHandler { name = 'AddonBlockTags'; blockName = 'tags'; - constructor(private translate: TranslateService) { + constructor() { super(); } @@ -44,7 +43,7 @@ export class AddonBlockTagsHandler extends CoreBlockBaseHandler { : CoreBlockHandlerData | Promise { return { - title: this.translate.instant('addon.block_tags.pluginname'), + title: 'addon.block_tags.pluginname', class: 'addon-block-tags', component: CoreBlockPreRenderedComponent }; diff --git a/src/core/block/components/pre-rendered-block/core-block-pre-rendered.html b/src/core/block/components/pre-rendered-block/core-block-pre-rendered.html index 84780cabb5e..84cf2ac5203 100644 --- a/src/core/block/components/pre-rendered-block/core-block-pre-rendered.html +++ b/src/core/block/components/pre-rendered-block/core-block-pre-rendered.html @@ -1,5 +1,5 @@ -

+

diff --git a/src/core/siteplugins/classes/handlers/block-handler.ts b/src/core/siteplugins/classes/handlers/block-handler.ts index 8ee9dd059f8..b4734d36ead 100644 --- a/src/core/siteplugins/classes/handlers/block-handler.ts +++ b/src/core/siteplugins/classes/handlers/block-handler.ts @@ -15,14 +15,17 @@ import { Injector } from '@angular/core'; import { CoreSitePluginsBaseHandler } from './base-handler'; import { CoreBlockHandler, CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; import { CoreSitePluginsBlockComponent } from '@core/siteplugins/components/block/block'; +import { CoreSitePluginsOnlyTitleBlockComponent } from '@core/siteplugins/components/only-title-block/only-title-block'; /** * Handler to support a block using a site plugin. */ export class CoreSitePluginsBlockHandler extends CoreSitePluginsBaseHandler implements CoreBlockHandler { - constructor(name: string, public blockName: string, protected handlerSchema: any, protected initResult: any) { + constructor(name: string, public title: string, public blockName: string, protected handlerSchema: any, + protected initResult: any) { super(name); } @@ -38,23 +41,27 @@ export class CoreSitePluginsBlockHandler extends CoreSitePluginsBaseHandler impl */ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number): CoreBlockHandlerData | Promise { - let title, - className; - if (this.handlerSchema.displaydata && this.handlerSchema.displaydata.title) { - title = this.handlerSchema.displaydata.title; - } else { - title = 'plugins.block_' + block.name + '.pluginname'; - } + let className, + component; + if (this.handlerSchema.displaydata && this.handlerSchema.displaydata.class) { className = this.handlerSchema.displaydata.class; } else { className = 'block_' + block.name; } + if (this.handlerSchema.displaydata && this.handlerSchema.displaydata.type == 'title') { + component = CoreSitePluginsOnlyTitleBlockComponent; + } else if (this.handlerSchema.displaydata && this.handlerSchema.displaydata.type == 'prerendered') { + component = CoreBlockPreRenderedComponent; + } else { + component = CoreSitePluginsBlockComponent; + } + return { - title: title, + title: this.title, class: className, - component: CoreSitePluginsBlockComponent + component: component }; } } diff --git a/src/core/siteplugins/components/block/block.ts b/src/core/siteplugins/components/block/block.ts index 807bceba388..c3375a90f57 100644 --- a/src/core/siteplugins/components/block/block.ts +++ b/src/core/siteplugins/components/block/block.ts @@ -53,7 +53,10 @@ export class CoreSitePluginsBlockComponent extends CoreBlockBaseComponent implem if (handler) { this.component = handler.plugin.component; this.method = handler.handlerSchema.method; - this.args = { }; + this.args = { + contextlevel: this.contextLevel, + instanceid: this.instanceId, + }; this.initResult = handler.initResult; } } diff --git a/src/core/siteplugins/components/components.module.ts b/src/core/siteplugins/components/components.module.ts index ebc7d03b9b9..abba3cd8ca9 100644 --- a/src/core/siteplugins/components/components.module.ts +++ b/src/core/siteplugins/components/components.module.ts @@ -30,12 +30,14 @@ import { CoreSitePluginsAssignFeedbackComponent } from './assign-feedback/assign import { CoreSitePluginsAssignSubmissionComponent } from './assign-submission/assign-submission'; import { CoreSitePluginsWorkshopAssessmentStrategyComponent } from './workshop-assessment-strategy/workshop-assessment-strategy'; import { CoreSitePluginsBlockComponent } from '@core/siteplugins/components/block/block'; +import { CoreSitePluginsOnlyTitleBlockComponent } from '@core/siteplugins/components/only-title-block/only-title-block'; @NgModule({ declarations: [ CoreSitePluginsPluginContentComponent, CoreSitePluginsModuleIndexComponent, CoreSitePluginsBlockComponent, + CoreSitePluginsOnlyTitleBlockComponent, CoreSitePluginsCourseOptionComponent, CoreSitePluginsCourseFormatComponent, CoreSitePluginsUserProfileFieldComponent, @@ -59,6 +61,7 @@ import { CoreSitePluginsBlockComponent } from '@core/siteplugins/components/bloc CoreSitePluginsPluginContentComponent, CoreSitePluginsModuleIndexComponent, CoreSitePluginsBlockComponent, + CoreSitePluginsOnlyTitleBlockComponent, CoreSitePluginsCourseOptionComponent, CoreSitePluginsCourseFormatComponent, CoreSitePluginsUserProfileFieldComponent, @@ -72,6 +75,7 @@ import { CoreSitePluginsBlockComponent } from '@core/siteplugins/components/bloc entryComponents: [ CoreSitePluginsModuleIndexComponent, CoreSitePluginsBlockComponent, + CoreSitePluginsOnlyTitleBlockComponent, CoreSitePluginsCourseOptionComponent, CoreSitePluginsCourseFormatComponent, CoreSitePluginsUserProfileFieldComponent, diff --git a/src/core/siteplugins/components/only-title-block/core-siteplugins-only-title-block.html b/src/core/siteplugins/components/only-title-block/core-siteplugins-only-title-block.html new file mode 100644 index 00000000000..287592371e4 --- /dev/null +++ b/src/core/siteplugins/components/only-title-block/core-siteplugins-only-title-block.html @@ -0,0 +1,3 @@ + +

{{ title | translate }}

+
\ No newline at end of file diff --git a/src/core/siteplugins/components/only-title-block/only-title-block.ts b/src/core/siteplugins/components/only-title-block/only-title-block.ts new file mode 100644 index 00000000000..b0ceaa45675 --- /dev/null +++ b/src/core/siteplugins/components/only-title-block/only-title-block.ts @@ -0,0 +1,69 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Injector, OnInit, Component, Optional } from '@angular/core'; +import { NavController } from 'ionic-angular'; +import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component'; +import { CoreSitePluginsProvider } from '../../providers/siteplugins'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; + +/** + * Component to render blocks with only a title and link. + */ +@Component({ + selector: 'core-siteplugins-only-title-block', + templateUrl: 'core-siteplugins-only-title-block.html' +}) +export class CoreSitePluginsOnlyTitleBlockComponent extends CoreBlockBaseComponent implements OnInit { + + constructor(injector: Injector, protected sitePluginsProvider: CoreSitePluginsProvider, + protected blockDelegate: CoreBlockDelegate, private navCtrl: NavController, + @Optional() private svComponent: CoreSplitViewComponent) { + + super(injector, 'CoreSitePluginsOnlyTitleBlockComponent'); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + super.ngOnInit(); + + this.fetchContentDefaultError = 'Error getting ' + this.block.contents.title + ' data.'; + } + + /** + * Go to the block page. + */ + gotoBlock(): void { + const handlerName = this.blockDelegate.getHandlerName(this.block.name); + const handler = this.sitePluginsProvider.getSitePluginHandler(handlerName); + + if (handler) { + const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; + + navCtrl.push('CoreSitePluginsPluginPage', { + title: this.title, + component: handler.plugin.component, + method: handler.handlerSchema.method, + initResult: handler.initResult, + args: { + contextlevel: this.contextLevel, + instanceid: this.instanceId, + }, + }); + } + } +} diff --git a/src/core/siteplugins/providers/helper.ts b/src/core/siteplugins/providers/helper.ts index 1ca20e3f2a4..f046cdbb444 100644 --- a/src/core/siteplugins/providers/helper.ts +++ b/src/core/siteplugins/providers/helper.ts @@ -661,10 +661,11 @@ export class CoreSitePluginsHelperProvider { string | Promise { const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), - blockName = (handlerSchema.moodlecomponent || plugin.component).replace('block_', ''); + blockName = (handlerSchema.moodlecomponent || plugin.component).replace('block_', ''), + prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'); this.blockDelegate.registerHandler( - new CoreSitePluginsBlockHandler(uniqueName, blockName, handlerSchema, initResult)); + new CoreSitePluginsBlockHandler(uniqueName, prefixedTitle, blockName, handlerSchema, initResult)); return uniqueName; } @@ -709,7 +710,7 @@ export class CoreSitePluginsHelperProvider { // Create and register the handler. const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), - prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title), + prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'), handler = new CoreSitePluginsCourseOptionHandler(uniqueName, prefixedTitle, plugin, handlerSchema, initResult, this.sitePluginsProvider, this.utils); @@ -749,7 +750,7 @@ export class CoreSitePluginsHelperProvider { // Create and register the handler. const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), - prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title); + prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'); this.mainMenuDelegate.registerHandler( new CoreSitePluginsMainMenuHandler(uniqueName, prefixedTitle, plugin, handlerSchema, initResult)); @@ -778,7 +779,7 @@ export class CoreSitePluginsHelperProvider { // Create and register the handler. const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), - prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title), + prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'), processorName = (handlerSchema.moodlecomponent || plugin.component).replace('message_', ''); this.messageOutputDelegate.registerHandler(new CoreSitePluginsMessageOutputHandler(uniqueName, processorName, @@ -897,7 +898,7 @@ export class CoreSitePluginsHelperProvider { // Create and register the handler. const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), - prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title); + prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'); this.settingsDelegate.registerHandler( new CoreSitePluginsSettingsHandler(uniqueName, prefixedTitle, plugin, handlerSchema, initResult)); @@ -926,7 +927,7 @@ export class CoreSitePluginsHelperProvider { // Create and register the handler. const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), - prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title), + prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'), handler = new CoreSitePluginsUserProfileHandler(uniqueName, prefixedTitle, plugin, handlerSchema, initResult, this.sitePluginsProvider, this.utils); From 5420dc225e6ce2dc18c2fd4d45abba706212492d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 31 Jul 2019 11:15:55 +0200 Subject: [PATCH 137/241] MOBILE-3097 config: Change demo sites URL --- src/config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.json b/src/config.json index 97284d65863..47588bf5fd7 100644 --- a/src/config.json +++ b/src/config.json @@ -58,12 +58,12 @@ "wsextservice": "local_mobile", "demo_sites": { "student": { - "url": "https:\/\/school.demo.moodle.net", + "url": "https:\/\/school.moodledemo.net", "username": "student", "password": "moodle" }, "teacher": { - "url": "https:\/\/school.demo.moodle.net", + "url": "https:\/\/school.moodledemo.net", "username": "teacher", "password": "moodle" } From 67cd70289cc15f5a4bef554e60ab605ced5f559d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 31 Jul 2019 12:07:04 +0200 Subject: [PATCH 138/241] MOBILE-3107 feedback: Show group selector for non editing teachers --- .../mod/feedback/components/index/addon-mod-feedback-index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html b/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html index bc403c9e012..8a70e2dfc55 100644 --- a/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html +++ b/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html @@ -45,7 +45,7 @@ - + {{ 'core.groupsseparate' | translate }} {{ 'core.groupsvisible' | translate }} From 532d1686b31d47a155a553367d3f30b8d28d26aa Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 31 Jul 2019 11:37:49 +0200 Subject: [PATCH 139/241] MOBILE-3028 quiz: Allow attempting quiz with unsupported questions --- scripts/langindex.json | 7 +++++++ .../components/index/addon-mod-quiz-index.html | 11 +++++++++-- src/addon/mod/quiz/components/index/index.ts | 6 +++++- src/addon/mod/quiz/lang/en.json | 4 +++- src/addon/mod/quiz/providers/quiz.ts | 16 ++++++++++++---- src/assets/lang/en.json | 4 +++- .../question/components/question/question.ts | 4 +++- 7 files changed, 42 insertions(+), 10 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 055b3b6896a..be9ec43d9d3 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -730,6 +730,7 @@ "addon.mod_quiz.attemptnumber": "quiz", "addon.mod_quiz.attemptquiznow": "quiz", "addon.mod_quiz.attemptstate": "quiz", + "addon.mod_quiz.canattemptbutnotsubmit": "local_moodlemobileapp", "addon.mod_quiz.cannotsubmitquizdueto": "local_moodlemobileapp", "addon.mod_quiz.clearchoice": "qtype_multichoice", "addon.mod_quiz.comment": "quiz", @@ -802,6 +803,7 @@ "addon.mod_quiz.warningattemptfinished": "local_moodlemobileapp", "addon.mod_quiz.warningdatadiscarded": "local_moodlemobileapp", "addon.mod_quiz.warningdatadiscardedfromfinished": "local_moodlemobileapp", + "addon.mod_quiz.warningquestionsnotsupported": "local_moodlemobileapp", "addon.mod_quiz.yourfinalgradeis": "quiz", "addon.mod_resource.errorwhileloadingthecontent": "local_moodlemobileapp", "addon.mod_resource.modifieddate": "resource", @@ -1318,7 +1320,12 @@ "core.comments.comments": "moodle", "core.comments.commentscount": "moodle", "core.comments.commentsnotworking": "local_moodlemobileapp", + "core.comments.deletecommentbyon": "moodle", + "core.comments.eventcommentcreated": "moodle", + "core.comments.eventcommentdeleted": "moodle", "core.comments.nocomments": "moodle", + "core.comments.savecomment": "moodle", + "core.comments.warningcommentsnotsent": "local_moodlemobileapp", "core.commentscount": "moodle", "core.commentsnotworking": "local_moodlemobileapp", "core.completion-alt-auto-fail": "completion", diff --git a/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html b/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html index bb204717c70..7f7cb5b7444 100644 --- a/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html +++ b/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html @@ -90,7 +90,7 @@

{{ 'addon.mod_quiz.summaryofattempts' | translate }}

{{ 'addon.mod_quiz.noquestions' | translate }}

- +

{{ 'addon.mod_quiz.errorquestionsnotsupported' | translate }}

{{ type }}

@@ -109,6 +109,13 @@

{{ 'addon.mod_quiz.summaryofattempts' | translate }}

{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}
+ + +

{{ 'addon.mod_quiz.canattemptbutnotsubmit' | translate }}

+

{{ 'addon.mod_quiz.warningquestionsnotsupported' | translate }}

+

{{ type }}

+
+ - + + + + +
From 949467a11bf5d0c8d8a1e83cb79f1da288f1bb43 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Tue, 6 Aug 2019 14:07:42 +0200 Subject: [PATCH 155/241] MOBILE-3021 calendar: Open day when clicking any part of the cell --- .../components/calendar/addon-calendar-calendar.html | 8 ++++---- src/addon/calendar/components/calendar/calendar.ts | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index 5872e86d0d6..073811937ee 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -38,8 +38,8 @@

{{ periodName }}

- -

{{ day.mday }}

+ +

{{ day.mday }}

@@ -47,7 +47,7 @@

{{ periodName }}

-

+

@@ -56,7 +56,7 @@

{{ periodName }}

{{event.name}}

-

{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}

+

{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}

diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index dcdb412b16f..5507a389f23 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -305,10 +305,12 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest /** * An event was clicked. * - * @param {any} event Event. + * @param {any} calendarEvent Calendar event.. + * @param {MouseEvent} event Mouse event. */ - eventClicked(event: any): void { - this.onEventClicked.emit(event.id); + eventClicked(calendarEvent: any, event: MouseEvent): void { + this.onEventClicked.emit(calendarEvent.id); + event.stopPropagation(); } /** From 0521fda729d1980dcb581d3de85fe0dee61f9184 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Tue, 6 Aug 2019 14:38:31 +0200 Subject: [PATCH 156/241] MOBILE-3021 calendar: Display current month/day button before other buttons --- .../components/calendar/addon-calendar-calendar.html | 2 +- src/addon/calendar/pages/day/day.html | 6 +++--- src/components/navbar-buttons/navbar-buttons.ts | 5 ++++- src/providers/utils/dom.ts | 6 ++++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index 073811937ee..1bf31b5d08c 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -1,6 +1,6 @@ - + diff --git a/src/addon/calendar/pages/day/day.html b/src/addon/calendar/pages/day/day.html index 083bdd3159f..cec0955aca7 100644 --- a/src/addon/calendar/pages/day/day.html +++ b/src/addon/calendar/pages/day/day.html @@ -2,12 +2,12 @@ {{ 'addon.calendar.calendarevents' | translate }} - + diff --git a/src/components/navbar-buttons/navbar-buttons.ts b/src/components/navbar-buttons/navbar-buttons.ts index 7f4ae239be7..c38931322a7 100644 --- a/src/components/navbar-buttons/navbar-buttons.ts +++ b/src/components/navbar-buttons/navbar-buttons.ts @@ -25,6 +25,8 @@ import { CoreContextMenuComponent } from '../context-menu/context-menu'; * If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that * position. If no start/end is specified, then the buttons will be added to the first found in the header. * + * If this component has a "prepend" attribute, the buttons will be added before other existing buttons in the header. + * * You can use the [hidden] input to hide all the inner buttons if a certain condition is met. * * IMPORTANT: Do not use *ngIf in the buttons inside this component, it can cause problems. Please use [hidden] instead. @@ -92,7 +94,8 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy { if (buttonsContainer) { this.mergeContextMenus(buttonsContainer); - this.movedChildren = this.domUtils.moveChildren(this.element, buttonsContainer); + const prepend = this.element.hasAttribute('prepend'); + this.movedChildren = this.domUtils.moveChildren(this.element, buttonsContainer, prepend); this.showHideAllElements(); } else { diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index d67c7fb9004..e0a99c94c7e 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -795,16 +795,18 @@ export class CoreDomUtilsProvider { * * @param {HTMLElement} oldParent The old parent. * @param {HTMLElement} newParent The new parent. + * @param {boolean} [prepend] If true, adds the children to the beginning of the new parent. * @return {Node[]} List of moved children. */ - moveChildren(oldParent: HTMLElement, newParent: HTMLElement): Node[] { + moveChildren(oldParent: HTMLElement, newParent: HTMLElement, prepend?: boolean): Node[] { const movedChildren: Node[] = []; + const referenceNode = prepend ? newParent.firstChild : null; while (oldParent.childNodes.length > 0) { const child = oldParent.childNodes[0]; movedChildren.push(child); - newParent.appendChild(child); + newParent.insertBefore(child, referenceNode); } return movedChildren; From 8ae300999679674c231952c77dc59eba70d2a0b8 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 7 Aug 2019 11:41:45 +0200 Subject: [PATCH 157/241] MOBILE-3021 calendar: Fit whole calendar in 5 inch phones --- .../components/calendar/addon-calendar-calendar.html | 6 +++--- src/addon/calendar/components/calendar/calendar.scss | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index 1bf31b5d08c..bf5c2942256 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -8,8 +8,8 @@ - - + + @@ -31,7 +31,7 @@

{{ periodName }}

-

{{ day.shortname | translate }}

+ {{ day.shortname | translate }}
diff --git a/src/addon/calendar/components/calendar/calendar.scss b/src/addon/calendar/components/calendar/calendar.scss index f96cba14d4f..278e56bcf12 100644 --- a/src/addon/calendar/components/calendar/calendar.scss +++ b/src/addon/calendar/components/calendar/calendar.scss @@ -10,6 +10,10 @@ $calendar-border-color: $gray !default; ion-app.app-root addon-calendar-calendar { + .addon-calendar-navigation { + @include padding(5px, 10px, null, 10px); + } + .addon-calendar-months { background-color: white; padding: 0; @@ -19,7 +23,7 @@ ion-app.app-root addon-calendar-calendar { border-bottom: 1px solid $calendar-border-color; @include border-end(1px, solid, $calendar-border-color); overflow: hidden; - min-height: 70px; + min-height: 60px; &:first-child { @include padding(null, null, null, 10px); @@ -64,7 +68,7 @@ ion-app.app-root addon-calendar-calendar { } .addon-calendar-dot-types { - @include margin(0.6em, null, 0.6em, null); + margin: 0; } } @@ -125,6 +129,7 @@ ion-app.app-root addon-calendar-calendar { @include margin-horizontal(1px, 1px); width: 16px; height: 16px; - display: inline; + display: inline-block; + vertical-align: middle; } } \ No newline at end of file From 6e80f34547e21c946d177941faeb6e5f5053a4d6 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 7 Aug 2019 12:11:12 +0200 Subject: [PATCH 158/241] MOBILE-3021 calendar: Fix filter events by course --- src/addon/calendar/providers/helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index 1a83ac4fe43..00005972a3b 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -274,7 +274,7 @@ export class AddonCalendarHelperProvider { } // Show the event if it is from site home or if it matches the selected course. - return event.courseid === this.sitesProvider.getSiteHomeId() || event.courseid == courseId; + return event.course && (event.course.id == this.sitesProvider.getCurrentSiteHomeId() || event.course.id == courseId); } /** From e5f5f31a10bf1ad2c802ecf057a1f1a853e7b822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 7 Aug 2019 12:32:36 +0200 Subject: [PATCH 159/241] MOBILE-3025 blocks: Limit block usage to 3.7 onwards --- src/core/course/providers/course.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index 9ddb17e59a5..f812ef2b6c0 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -114,10 +114,11 @@ export class CoreCourseProvider { * Check if the get course blocks WS is available in current site. * * @return {boolean} Whether it's available. - * @since 3.3 + * @since 3.7 */ canGetCourseBlocks(): boolean { - return this.sitesProvider.wsAvailableInCurrentSite('core_block_get_course_blocks'); + return this.sitesProvider.getCurrentSite().isVersionGreaterEqualThan('3.7') && + this.sitesProvider.wsAvailableInCurrentSite('core_block_get_course_blocks'); } /** @@ -267,7 +268,7 @@ export class CoreCourseProvider { * @param {number} courseId Course ID. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the list of blocks. - * @since 3.3 + * @since 3.7 */ getCourseBlocks(courseId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { From aff813617974221f5782533536c13d33f91bc4ab Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Thu, 8 Aug 2019 13:01:02 +0200 Subject: [PATCH 160/241] MOBILE-3068 styles: Remove bottom padding when keyboard is open --- src/components/ion-tabs/ion-tabs.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ion-tabs/ion-tabs.scss b/src/components/ion-tabs/ion-tabs.scss index 8e8a0ebd389..31f66765086 100644 --- a/src/components/ion-tabs/ion-tabs.scss +++ b/src/components/ion-tabs/ion-tabs.scss @@ -25,7 +25,7 @@ ion-app.app-root core-ion-tabs { &[tabsplacement="bottom"] { .ion-page > ion-content > .scroll-content { - margin-bottom: $navbar-md-height !important; + margin-bottom: $navbar-md-height; } } From de2c297b3cda973fe69ebb4932ba6e877b410ab4 Mon Sep 17 00:00:00 2001 From: Mark Johnson Date: Thu, 8 Aug 2019 16:11:14 +0100 Subject: [PATCH 161/241] MOBILE-3100 Accessibility: Fix issues when changing font sizes --- src/app/app.scss | 18 +++++++++++++---- src/components/ion-tabs/ion-tabs.scss | 1 + src/components/tabs/tabs.scss | 2 +- src/core/settings/pages/general/general.ts | 2 +- src/theme/variables.scss | 23 ++++++++++++++++++++++ 5 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/app/app.scss b/src/app/app.scss index 2d0a1baa004..30989ecaddc 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -649,13 +649,16 @@ ion-app.app-root { .toolbar img.core-bar-button-image, .toolbar .core-bar-button-image img { padding: 0; - width: 100%; - height: 100%; - max-width: $core-toolbar-button-image-width - 1; - max-height: $core-toolbar-button-image-width - 1; + width: $core-toolbar-button-image-width; + height: $core-toolbar-button-image-width; + max-width: $core-toolbar-button-image-width; border-radius: 50%; } + .toolbar-ios { + height: 52px; + } + // Footer with auto height. .footer.footer-adjustable { height: auto; @@ -1147,3 +1150,10 @@ ion-app.platform-desktop { .ion-md-funnel::before { content: "\f182"; } + +// Fix icon size in lists, to prevent them scaling with text. +.item, .item-inner { + > ion-icon { + font-size: 28px; + } +} diff --git a/src/components/ion-tabs/ion-tabs.scss b/src/components/ion-tabs/ion-tabs.scss index 8e8a0ebd389..6470d8a4f4c 100644 --- a/src/components/ion-tabs/ion-tabs.scss +++ b/src/components/ion-tabs/ion-tabs.scss @@ -3,6 +3,7 @@ $core-sidetab-size: 60px !default; ion-app.app-root core-ion-tabs { .tabbar { z-index: 101; // For some reason, the regular z-index isn't enough with our tabs, use a higher one. + height: 56px; .core-ion-tabs-loading { height: 100%; diff --git a/src/components/tabs/tabs.scss b/src/components/tabs/tabs.scss index ac4b2ac5463..01568394df4 100644 --- a/src/components/tabs/tabs.scss +++ b/src/components/tabs/tabs.scss @@ -74,7 +74,7 @@ ion-app.app-root.ios .core-tabs-bar .tab-slide { max-width: $tabs-ios-tab-max-width; min-height: $tabs-ios-tab-min-height; - font-size: $tabs-ios-tab-font-size + 4; + font-size: $tabs-ios-tab-font-size; font-weight: $tabs-ios-tab-font-weight; color: $tabs-ios-tab-text-color; } diff --git a/src/core/settings/pages/general/general.ts b/src/core/settings/pages/general/general.ts index b752f8d5e6f..9befe638a1d 100644 --- a/src/core/settings/pages/general/general.ts +++ b/src/core/settings/pages/general/general.ts @@ -68,7 +68,7 @@ export class CoreSettingsGeneralPage { this.selectedLanguage = currentLanguage; }); - this.configProvider.get(CoreConstants.SETTINGS_FONT_SIZE, CoreConfigConstants.font_sizes[0]).then((fontSize) => { + this.configProvider.get(CoreConstants.SETTINGS_FONT_SIZE, CoreConfigConstants.font_sizes[0].toString()).then((fontSize) => { this.selectedFontSize = fontSize; this.fontSizes = CoreConfigConstants.font_sizes.map((size) => { return { diff --git a/src/theme/variables.scss b/src/theme/variables.scss index a094ae0ffc3..1727a6d2894 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -229,6 +229,29 @@ $popover-wp-width: $popover-width; $item-wp-divider-background: $item-divider-background; $item-wp-divider-color: $item-divider-color; +// Font sizes +// --------------------------------------------------- +// Some font sizes are defined in absolute pixels by ionic, +// override these with relative sizes so they are resizable. +$alert-ios-message-font-size: 1.4rem; +$alert-ios-title-font-size: 2.2rem; +$alert-ios-sub-title-font-size: 1.6rem; +$alert-md-message-font-size: 1.4rem; +$alert-md-title-font-size: 2.2rem; +$alert-md-sub-title-font-size: 1.6rem; +$alert-button-font-size: 1.4rem; +$tabs-ios-tab-font-size: 1.4rem; +$chip-ios-font-size: 1.3rem; +$chip-md-font-size: 1.3rem; + +// Icon sizes +// --------------------------------------------------- +// Some font icons have relative sizes set by ionic, +// define absolute sizes so they aren't scaled with text. +$tabs-md-tab-icon-size: 24px; +$tabs-md-tab-min-height: 56px; +$tabs-ios-tab-min-height: 56px; + // App Theme // -------------------------------------------------- // Ionic apps can have different themes applied, which can From 184faa4a6db10ed9ccac3e7f4a3fe44d0752a126 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Thu, 8 Aug 2019 13:00:03 +0200 Subject: [PATCH 162/241] MOBILE-3042 styles: Fix ion-item-divider displayed over video menus --- src/app/app.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app/app.scss b/src/app/app.scss index 2d0a1baa004..cf34c2ee795 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -1079,6 +1079,11 @@ details summary { contain: none !important; } +// Lower z-index for ion-item-divider so it is displayed below video menus. +ion-item-divider { + z-index: 2; // Ionic default is 100. +} + // Highlight text. .matchtext { background-color: $core-text-hightlight-background-color; From 2b72b65ba501d33d9a58337cb9f2e92dc5508273 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Thu, 8 Aug 2019 10:49:35 +0200 Subject: [PATCH 163/241] MOBILE-1927 calendar: Invalidate day when creating/editing an event --- src/addon/calendar/providers/helper.ts | 98 +++++++++++++++----------- 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index 00005972a3b..4ac36ec701e 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -18,6 +18,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreCourseProvider } from '@core/course/providers/course'; import { AddonCalendarProvider } from './calendar'; import { CoreConstants } from '@core/constants'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import * as moment from 'moment'; /** @@ -38,7 +39,8 @@ export class AddonCalendarHelperProvider { constructor(logger: CoreLoggerProvider, private courseProvider: CoreCourseProvider, private sitesProvider: CoreSitesProvider, - private calendarProvider: AddonCalendarProvider) { + private calendarProvider: AddonCalendarProvider, + private utils: CoreUtilsProvider) { this.logger = logger.getInstance('AddonCalendarHelperProvider'); } @@ -286,57 +288,67 @@ export class AddonCalendarHelperProvider { * @return {Promise} REsolved when done. */ invalidateRepeatedEventsOnCalendar(event: any, repeated: number, siteId?: string): Promise { - let invalidatePromise; - const timestarts = []; - - if (repeated > 1) { - if (event.repeatid) { - // Being edited or deleted. - invalidatePromise = this.calendarProvider.getLocalEventsByRepeatIdFromLocalDb(event.repeatid, siteId) - .then((events) => { - return events.map((event) => { - timestarts.push(event.timestart); - - return this.calendarProvider.invalidateEvent(event.id); + return this.sitesProvider.getSite(siteId).then((site) => { + let invalidatePromise; + const timestarts = []; + + if (repeated > 1) { + if (event.repeatid) { + // Being edited or deleted. + invalidatePromise = this.calendarProvider.getLocalEventsByRepeatIdFromLocalDb(event.repeatid, site.id) + .then((events) => { + return this.utils.allPromises(events.map((event) => { + timestarts.push(event.timestart); + + return this.calendarProvider.invalidateEvent(event.id); + })); }); + } else { + // Being added. + let time = event.timestart; + while (repeated > 0) { + timestarts.push(time); + time += CoreConstants.SECONDS_DAY * 7; + repeated--; + } - }); - } else { - // Being added. - let time = event.timestart; - while (repeated > 0) { - timestarts.push(time); - time += CoreConstants.SECONDS_DAY * 7; - repeated--; + invalidatePromise = Promise.resolve(); } - - invalidatePromise = Promise.resolve(); + } else { + // Not repeated. + timestarts.push(event.timestart); + invalidatePromise = this.calendarProvider.invalidateEvent(event.id); } - } else { - // Not repeated. - timestarts.push(event.timestart); - invalidatePromise = this.calendarProvider.invalidateEvent(event.id); - } - return invalidatePromise.then(() => { - let lastMonth, lastYear; + return invalidatePromise.finally(() => { + let lastMonth, lastYear; - return Promise.all([ - this.calendarProvider.invalidateAllUpcomingEvents(), - timestarts.map((time) => { - const day = moment(new Date(time * 1000)); + return this.utils.allPromises([ + this.calendarProvider.invalidateAllUpcomingEvents(), - if (lastMonth && (lastMonth == day.month() + 1 && lastYear == day.year())) { - return Promise.resolve(); - } + // Invalidate months. + this.utils.allPromises(timestarts.map((time) => { + const day = moment(new Date(time * 1000)); + + if (lastMonth && (lastMonth == day.month() + 1 && lastYear == day.year())) { + return Promise.resolve(); + } + + // Invalidate once. + lastMonth = day.month() + 1; + lastYear = day.year(); - // Invalidate once. - lastMonth = day.month() + 1; - lastYear = day.year(); + return this.calendarProvider.invalidateMonthlyEvents(lastYear, lastMonth, site.id); + })), - return this.calendarProvider.invalidateMonthlyEvents(lastYear, lastMonth, siteId); - }) - ]); + // Invalidate days. + this.utils.allPromises(timestarts.map((time) => { + const day = moment(new Date(time * 1000)); + + return this.calendarProvider.invalidateDayEvents(day.year(), day.month() + 1, day.date(), site.id); + })), + ]); + }); }); } } From 4e3f57533b6900848c1223642072074691c389e0 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Thu, 8 Aug 2019 12:01:01 +0200 Subject: [PATCH 164/241] MOBILE-1927 calendar: Fix offline events not displayed in day view --- src/addon/calendar/pages/day/day.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addon/calendar/pages/day/day.ts b/src/addon/calendar/pages/day/day.ts index a8c1e90a1bb..f5a302997fe 100644 --- a/src/addon/calendar/pages/day/day.ts +++ b/src/addon/calendar/pages/day/day.ts @@ -321,7 +321,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { protected mergeEvents(): any[] { this.hasOffline = false; - if (!this.offlineEditedEventsIds.length && !this.deletedEvents.length) { + if (!Object.keys(this.offlineEvents).length && !this.deletedEvents.length) { // No offline events, nothing to merge. return this.onlineEvents; } From de1207f8beeed9e1657e34ef40a99eafe24b9b9b Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Thu, 8 Aug 2019 12:54:10 +0200 Subject: [PATCH 165/241] MOBILE-1927 calendar: Fix no groups message --- src/assets/lang/en.json | 2 +- src/lang/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 1ea76b5baf0..6d03d0b2252 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1391,7 +1391,7 @@ "core.course.warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.", "core.course.warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}", "core.coursedetails": "Course details", - "core.coursenogroups": "This course doesn't have any group.", + "core.coursenogroups": "You are not a member of any group of this course.", "core.courses.addtofavourites": "Star this course", "core.courses.allowguests": "This course allows guest users to enter", "core.courses.availablecourses": "Available courses", diff --git a/src/lang/en.json b/src/lang/en.json index da67374656f..8c5698b50ef 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -48,7 +48,7 @@ "copiedtoclipboard": "Text copied to clipboard", "course": "Course", "coursedetails": "Course details", - "coursenogroups": "This course doesn't have any group.", + "coursenogroups": "You are not a member of any group of this course.", "currentdevice": "Current device", "datastoredoffline": "Data stored in the device because it couldn't be sent. It will be sent automatically later.", "date": "Date", From 56d8aba3b40c4e020c3ccf1e09e9094c25d5450c Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 9 Aug 2019 11:50:08 +0200 Subject: [PATCH 166/241] NOBILE-3087 calendar: Allow navigating to all days and months in offline --- .../calendar/components/calendar/calendar.ts | 14 ++++- src/addon/calendar/pages/day/day.ts | 9 ++- src/addon/calendar/providers/calendar.ts | 6 ++ src/addon/calendar/providers/helper.ts | 62 +++++++++++++++++++ 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 5507a389f23..a620fe6eee9 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -23,6 +23,7 @@ import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreAppProvider } from '@providers/app'; /** * Component that displays a calendar. @@ -70,7 +71,8 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest private domUtils: CoreDomUtilsProvider, private timeUtils: CoreTimeUtilsProvider, private utils: CoreUtilsProvider, - private coursesProvider: CoreCoursesProvider) { + private coursesProvider: CoreCoursesProvider, + private appProvider: CoreAppProvider) { this.currentSiteId = sitesProvider.getCurrentSiteId(); @@ -184,8 +186,14 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest */ fetchEvents(): Promise { // Don't pass courseId and categoryId, we'll filter them locally. - return this.calendarProvider.getMonthlyEvents(this.year, this.month).then((result) => { - + return this.calendarProvider.getMonthlyEvents(this.year, this.month).catch((error) => { + if (!this.appProvider.isOnline()) { + // Allow navigating to non-cached months in offline (behave as if using emergency cache). + return this.calendarHelper.getOfflineMonthWeeks(this.year, this.month); + } else { + return Promise.reject(error); + } + }).then((result) => { // Calculate the period name. We don't use the one in result because it's in server's language. this.periodName = this.timeUtils.userDate(new Date(this.year, this.month - 1).getTime(), 'core.strftimemonthyear'); diff --git a/src/addon/calendar/pages/day/day.ts b/src/addon/calendar/pages/day/day.ts index f5a302997fe..73202cfd35a 100644 --- a/src/addon/calendar/pages/day/day.ts +++ b/src/addon/calendar/pages/day/day.ts @@ -282,7 +282,14 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { */ fetchEvents(): Promise { // Don't pass courseId and categoryId, we'll filter them locally. - return this.calendarProvider.getDayEvents(this.year, this.month, this.day).then((result) => { + return this.calendarProvider.getDayEvents(this.year, this.month, this.day).catch((error) => { + if (!this.appProvider.isOnline()) { + // Allow navigating to non-cached days in offline (behave as if using emergency cache). + return Promise.resolve({ events: [] }); + } else { + return Promise.reject(error); + } + }).then((result) => { const promises = []; // Calculate the period name. We don't use the one in result because it's in server's language. diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 9ecc4effd21..5ffe6d29dd0 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -43,6 +43,7 @@ export class AddonCalendarProvider { static DEFAULT_NOTIFICATION_TIME_CHANGED = 'AddonCalendarDefaultNotificationTimeChangedEvent'; static DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime'; static DEFAULT_NOTIFICATION_TIME = 60; + static STARTING_WEEK_DAY = 'addon_calendar_starting_week_day'; static NEW_EVENT_EVENT = 'addon_calendar_new_event'; static NEW_EVENT_DISCARDED_EVENT = 'addon_calendar_new_event_discarded'; static EDIT_EVENT_EVENT = 'addon_calendar_edit_event'; @@ -1198,6 +1199,11 @@ export class AddonCalendarProvider { }); }); + // Store starting week day preference, we need it in offline to show months that are not in cache. + if (this.appProvider.isOnline()) { + this.configProvider.set(AddonCalendarProvider.STARTING_WEEK_DAY, response.daynames[0].dayno); + } + return response; }); }); diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index 4ac36ec701e..c3b24c9569a 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -18,6 +18,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreCourseProvider } from '@core/course/providers/course'; import { AddonCalendarProvider } from './calendar'; import { CoreConstants } from '@core/constants'; +import { CoreConfigProvider } from '@providers/config'; import { CoreUtilsProvider } from '@providers/utils/utils'; import * as moment from 'moment'; @@ -40,6 +41,7 @@ export class AddonCalendarHelperProvider { private courseProvider: CoreCourseProvider, private sitesProvider: CoreSitesProvider, private calendarProvider: AddonCalendarProvider, + private configProvider: CoreConfigProvider, private utils: CoreUtilsProvider) { this.logger = logger.getInstance('AddonCalendarHelperProvider'); } @@ -191,6 +193,66 @@ export class AddonCalendarHelperProvider { return year + '#' + month; } + /** + * Get weeks of a month in offline (with no events). + * + * The result has the same structure than getMonthlyEvents, but it only contains fields that are actually used by the app. + * + * @param {number} year Year to get. + * @param {number} month Month to get. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the response. + */ + getOfflineMonthWeeks(year: number, month: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + // Get starting week day user preference, fallback to site configuration. + const startWeekDay = site.getStoredConfig('calendar_startwday'); + + return this.configProvider.get(AddonCalendarProvider.STARTING_WEEK_DAY, startWeekDay); + }).then((startWeekDay) => { + const today = moment(); + const isCurrentMonth = today.year() == year && today.month() == month - 1; + const weeks = []; + + let date = moment({year, month: month - 1, date: 1}); + for (let mday = 1; mday <= date.daysInMonth(); mday++) { + date = moment({year, month: month - 1, date: mday}); + + // Add new week and calculate prepadding. + if (!weeks.length || date.day() == startWeekDay) { + const prepaddingLength = (date.day() - startWeekDay + 7) % 7; + const prepadding = []; + for (let i = 0; i < prepaddingLength; i++) { + prepadding.push(i); + } + weeks.push({ prepadding, postpadding: [], days: []}); + } + + // Calculate postpadding of last week. + if (mday == date.daysInMonth()) { + const postpaddingLength = (startWeekDay - date.day() + 6) % 7; + const postpadding = []; + for (let i = 0; i < postpaddingLength; i++) { + postpadding.push(i); + } + weeks[weeks.length - 1].postpadding = postpadding; + } + + // Add day to current week. + weeks[weeks.length - 1].days.push({ + events: [], + hasevents: false, + mday: date.date(), + isweekend: date.day() == 0 || date.day() == 6, + istoday: isCurrentMonth && today.date() == date.date(), + calendareventtypes: [], + }); + } + + return {weeks, daynames: [{dayno: startWeekDay}]}; + }); + } + /** * Check if the data of an event has changed. * From e217234f9e03340a52a16ec530f7f0bb54884ec8 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 9 Aug 2019 13:09:07 +0200 Subject: [PATCH 167/241] MOBILE-3087 calendar: Fix offline events not displayed in upcoming events page --- .../calendar/components/upcoming-events/upcoming-events.ts | 2 +- src/addon/calendar/providers/calendar.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/addon/calendar/components/upcoming-events/upcoming-events.ts b/src/addon/calendar/components/upcoming-events/upcoming-events.ts index d540dae6e8f..74db1aebcdc 100644 --- a/src/addon/calendar/components/upcoming-events/upcoming-events.ts +++ b/src/addon/calendar/components/upcoming-events/upcoming-events.ts @@ -264,7 +264,7 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, return this.onlineEvents; } - const start = Date.now(), + const start = Date.now() / 1000, end = start + (CoreConstants.SECONDS_DAY * this.lookAhead); let result = this.onlineEvents; diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 5ffe6d29dd0..7b09df4a852 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -727,7 +727,7 @@ export class AddonCalendarProvider { return this.userProvider.getUserPreference('calendar_lookahead').catch((error) => { // Ignore errors. }).then((value): any => { - if (typeof value != 'undefined') { + if (value != null) { return value; } From 798f3b2d53827c2d4db56a5d73bdeafcb67d6e20 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 9 Aug 2019 13:15:49 +0200 Subject: [PATCH 168/241] MOBILE-3087 calendar: Changed title of upcoming events page --- src/addon/calendar/pages/index/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html index adfbe89db1b..fc6386e368a 100644 --- a/src/addon/calendar/pages/index/index.html +++ b/src/addon/calendar/pages/index/index.html @@ -1,6 +1,6 @@ - {{ 'addon.calendar.calendarevents' | translate }} + {{ (showCalendar ? 'addon.calendar.calendarevents' : 'addon.calendar.upcomingevents') | translate }}
- - - - - - - - - - +
+
+ + + + + + + + +
+
diff --git a/src/core/block/components/course-blocks/course-blocks.scss b/src/core/block/components/course-blocks/course-blocks.scss index e9f9e228a05..cc2c9f3c08e 100644 --- a/src/core/block/components/course-blocks/course-blocks.scss +++ b/src/core/block/components/course-blocks/course-blocks.scss @@ -1,8 +1,5 @@ -$core-side-blocks-max-small-width: 300px; -$core-side-blocks-min-small-width: 25%; - -$core-side-blocks-max-width: 320px; -$core-side-blocks-min-width: 30%; +$core-side-blocks-max-width: 30%; +$core-side-blocks-min-width: 280px; .core-course-block-with-blocks > .scroll-content { overflow-y: visible; @@ -10,15 +7,8 @@ $core-side-blocks-min-width: 30%; ion-app.app-root core-block-course-blocks { - &.core-no-blocks { - .core-course-blocks-content > ion-content { - height: auto; - - > .scroll-content { - overflow-y: visible; - position: relative; - } - } + &.core-no-blocks .core-course-blocks-content { + height: auto; } &.core-has-blocks { @@ -33,49 +23,51 @@ ion-app.app-root core-block-course-blocks { flex-wrap: nowrap; .core-course-blocks-content { - min-width: calc(100% - #{($core-side-blocks-max-small-width)}); - max-width: calc(100% - #{($core-side-blocks-min-small-width)}); - z-index: 0; - flex: 1; box-shadow: none !important; + flex-grow: 1; + max-width: 100%; } - ion-content.core-course-blocks-side { - transform: none !important; - position: sticky; - @include position(0, 0, 0, auto); - z-index: 30; - max-width: $core-side-blocks-max-small-width; - min-width: $core-side-blocks-min-small-width; - @include border-start(1px, solid, $list-md-border-color); - } - } - - @include media-breakpoint-up(lg) { - .core-course-blocks-content { - min-width: calc(100% - #{($core-side-blocks-max-width)}); - max-width: calc(100% - #{($core-side-blocks-min-width)}); - } - - ion-content.core-course-blocks-side { + div.core-course-blocks-side { + position: relative; + @include position(auto, 0, auto, auto); max-width: $core-side-blocks-max-width; min-width: $core-side-blocks-min-width; + @include border-start(1px, solid, $list-md-border-color); + + .core-course-blocks-side-scroll { + position: absolute; + top: 0; + max-width: 100%; + min-width: 100%; + + &.core-course-blocks-fixed-bottom { + position: fixed; + bottom: 0; + top: auto; + transform: none !important; + } + + core-block { + max-width: $core-side-blocks-max-width; + min-width: $core-side-blocks-min-width; + } + } } } @include media-breakpoint-down(sm) { // Disable scroll on individual columns. - .core-course-blocks-content > ion-content, - ion-content.core-course-blocks-side { + div.core-course-blocks-side { + transform: none !important; height: auto; &.core-hide-blocks { display: none; } - > .scroll-content { - overflow-y: visible; - position: relative; + .core-course-blocks-side-scroll { + transform: none !important; } } } diff --git a/src/core/block/components/course-blocks/course-blocks.ts b/src/core/block/components/course-blocks/course-blocks.ts index 744dd4326b8..574dc3c8bcc 100644 --- a/src/core/block/components/course-blocks/course-blocks.ts +++ b/src/core/block/components/course-blocks/course-blocks.ts @@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, ViewChildren, Input, OnInit, QueryList, ElementRef, Optional } from '@angular/core'; +import { Component, ViewChildren, Input, OnInit, QueryList, ElementRef, OnDestroy } from '@angular/core'; import { Content } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreAppProvider } from '@providers/app'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreBlockComponent } from '../block/block'; import { CoreBlockHelperProvider } from '../../providers/helper'; @@ -26,7 +27,7 @@ import { CoreBlockHelperProvider } from '../../providers/helper'; selector: 'core-block-course-blocks', templateUrl: 'core-block-course-blocks.html', }) -export class CoreBlockCourseBlocksComponent implements OnInit { +export class CoreBlockCourseBlocksComponent implements OnInit, OnDestroy { @Input() courseId: number; @Input() hideBlocks = false; @@ -38,13 +39,17 @@ export class CoreBlockCourseBlocksComponent implements OnInit { blocks = []; protected element: HTMLElement; - protected parentContent: HTMLElement; + protected lastScroll; + protected translationY = 0; + protected blocksScrollHeight = 0; + protected sideScroll: HTMLElement; + protected vpHeight = 0; // Viewport height. + protected scrollWorking = false; constructor(private domUtils: CoreDomUtilsProvider, private courseProvider: CoreCourseProvider, protected blockHelper: CoreBlockHelperProvider, element: ElementRef, - @Optional() content: Content) { + protected content: Content, protected appProvider: CoreAppProvider) { this.element = element.nativeElement; - this.parentContent = content.getElementRef().nativeElement; } /** @@ -53,9 +58,77 @@ export class CoreBlockCourseBlocksComponent implements OnInit { ngOnInit(): void { this.loadContent().finally(() => { this.dataLoaded = true; + + window.addEventListener('resize', this.initScroll.bind(this)); }); } + /** + * Setup scrolling. + */ + protected initScroll(): void { + const scroll: HTMLElement = this.content && this.content.getScrollElement(); + + this.domUtils.waitElementToExist(() => scroll && scroll.querySelector('.core-course-blocks-side')).then((sideElement) => { + const contentHeight = parseInt(this.content.getNativeElement().querySelector('.scroll-content').scrollHeight, 10); + + this.sideScroll = scroll.querySelector('.core-course-blocks-side-scroll'); + this.blocksScrollHeight = this.sideScroll.scrollHeight; + this.vpHeight = sideElement.clientHeight; + + // Check if needed and event was not init before. + if (this.appProvider.isWide() && this.vpHeight && contentHeight > this.vpHeight && + this.blocksScrollHeight > this.vpHeight) { + if (typeof this.lastScroll == 'undefined') { + this.lastScroll = 0; + scroll.addEventListener('scroll', this.scrollFunction.bind(this)); + } + this.scrollWorking = true; + } else { + this.sideScroll.style.transform = 'translate(0, 0)'; + this.sideScroll.classList.remove('core-course-blocks-fixed-bottom'); + this.scrollWorking = false; + } + }); + } + + /** + * Scroll function that moves the sidebar if needed. + * + * @param {Event} e Event to get the target from. + */ + protected scrollFunction(e: Event): void { + if (!this.scrollWorking) { + return; + } + + const target: any = e.target, + top = parseInt(target.scrollTop, 10), + goingUp = top < this.lastScroll; + if (goingUp) { + this.sideScroll.classList.remove('core-course-blocks-fixed-bottom'); + if (top <= this.translationY ) { + // Fixed to top. + this.translationY = top; + this.sideScroll.style.transform = 'translate(0, ' + this.translationY + 'px)'; + } + } else if (top - this.translationY >= (this.blocksScrollHeight - this.vpHeight)) { + // Fixed to bottom. + this.sideScroll.classList.add('core-course-blocks-fixed-bottom'); + this.translationY = top - (this.blocksScrollHeight - this.vpHeight); + this.sideScroll.style.transform = 'translate(0, ' + this.translationY + 'px)'; + } + + this.lastScroll = top; + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + window.removeEventListener('resize', this.initScroll); + } + /** * Invalidate blocks data. * @@ -95,11 +168,13 @@ export class CoreBlockCourseBlocksComponent implements OnInit { this.element.classList.add('core-has-blocks'); this.element.classList.remove('core-no-blocks'); - this.parentContent.classList.add('core-course-block-with-blocks'); + this.content.getElementRef().nativeElement.classList.add('core-course-block-with-blocks'); + + this.initScroll(); } else { this.element.classList.remove('core-has-blocks'); this.element.classList.add('core-no-blocks'); - this.parentContent.classList.remove('core-course-block-with-blocks'); + this.content.getElementRef().nativeElement.classList.remove('core-course-block-with-blocks'); } }); } diff --git a/src/core/course/components/format/core-course-format.html b/src/core/course/components/format/core-course-format.html index 028dc27857b..2d01ef5a1ab 100644 --- a/src/core/course/components/format/core-course-format.html +++ b/src/core/course/components/format/core-course-format.html @@ -6,69 +6,67 @@ - - - - - - -
- - - + + + + + +
+ + + +
+
+ + + + +
+
-
+ + + + +
- - - -
- -
- - - -
+ +
+ + + +
- -
- - - - -
- - -
- - - - - + +
+ + + + - + + - -
- - - - - + +
+ + + + + -
- + diff --git a/src/core/sitehome/components/index/core-sitehome-index.html b/src/core/sitehome/components/index/core-sitehome-index.html index 389a968accd..7bc8292fe2f 100644 --- a/src/core/sitehome/components/index/core-sitehome-index.html +++ b/src/core/sitehome/components/index/core-sitehome-index.html @@ -1,30 +1,28 @@ - - - - - - - - + + + + + + + - - + + - - - - - - - - - - + + + + + + + + + - + + - - - + + diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index e0a99c94c7e..8776024e79d 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -702,6 +702,45 @@ export class CoreDomUtilsProvider { return this.instances[id]; } + /** + * Wait an element to exists using the findFunction. + * + * @param {Function} findFunction The function used to find the element. + * @return {Promise} Resolved if found, rejected if too many tries. + */ + waitElementToExist(findFunction: Function): Promise { + const promiseInterval = { + promise: null, + resolve: null, + reject: null + }; + + let tries = 100; + + promiseInterval.promise = new Promise((resolve, reject): void => { + promiseInterval.resolve = resolve; + promiseInterval.reject = reject; + }); + + const clear = setInterval(() => { + const element: HTMLElement = findFunction(); + + if (element) { + clearInterval(clear); + promiseInterval.resolve(element); + } else { + tries--; + + if (tries <= 0) { + clearInterval(clear); + promiseInterval.reject(); + } + } + }, 100); + + return promiseInterval.promise; + } + /** * Handle bootstrap tooltips in a certain element. * diff --git a/src/theme/format-text.scss b/src/theme/format-text.scss index d14efd14877..a14ee433e86 100644 --- a/src/theme/format-text.scss +++ b/src/theme/format-text.scss @@ -9,6 +9,10 @@ ion-app.app-root core-rich-text-editor .core-rte-editor { margin-bottom: 1rem; } + .no-overflow { + overflow: auto; + } + // Fix lists styles in core-format-text. ul { padding-left: 1rem; From e270a1b1ff69a71656aec18291a1f2e17cd46924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 9 Aug 2019 14:58:55 +0200 Subject: [PATCH 170/241] MOBILE-3068 glossary: Add more options to glossary links --- .../glossary/providers/edit-link-handler.ts | 19 +++++++++++-------- .../glossary/providers/entry-link-handler.ts | 10 ++++++++-- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/addon/mod/glossary/providers/edit-link-handler.ts b/src/addon/mod/glossary/providers/edit-link-handler.ts index c2f34305c59..c86557aee79 100644 --- a/src/addon/mod/glossary/providers/edit-link-handler.ts +++ b/src/addon/mod/glossary/providers/edit-link-handler.ts @@ -18,6 +18,7 @@ import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { AddonModGlossaryProvider } from './glossary'; /** * Content links handler for glossary new entry. @@ -31,7 +32,7 @@ export class AddonModGlossaryEditLinkHandler extends CoreContentLinksHandlerBase pattern = /\/mod\/glossary\/edit\.php.*([\?\&](cmid)=\d+)/; constructor(private linkHelper: CoreContentLinksHelperProvider, private courseProvider: CoreCourseProvider, - private domUtils: CoreDomUtilsProvider) { + private domUtils: CoreDomUtilsProvider, private glossaryProvider: AddonModGlossaryProvider) { super(); } @@ -53,14 +54,16 @@ export class AddonModGlossaryEditLinkHandler extends CoreContentLinksHandlerBase cmId = parseInt(params.cmid, 10); this.courseProvider.getModuleBasicInfo(cmId, siteId).then((module) => { - const pageParams = { - courseId: module.course, - module: module, - glossary: module.module, - entry: null // It does not support entry editing. - }; + return this.glossaryProvider.getGlossary(module.course, module.id).then((glossary) => { + const pageParams = { + courseId: module.course, + module: module, + glossary: glossary, + entry: null // It does not support entry editing. + }; - return this.linkHelper.goInSite(navCtrl, 'AddonModGlossaryEditPage', pageParams, siteId); + this.linkHelper.goInSite(navCtrl, 'AddonModGlossaryEditPage', pageParams, siteId); + }); }).finally(() => { // Just in case. In fact we need to dismiss the modal before showing a toast or error message. modal.dismiss(); diff --git a/src/addon/mod/glossary/providers/entry-link-handler.ts b/src/addon/mod/glossary/providers/entry-link-handler.ts index 953aeb41a23..99acce7b731 100644 --- a/src/addon/mod/glossary/providers/entry-link-handler.ts +++ b/src/addon/mod/glossary/providers/entry-link-handler.ts @@ -27,7 +27,7 @@ import { AddonModGlossaryProvider } from './glossary'; export class AddonModGlossaryEntryLinkHandler extends CoreContentLinksHandlerBase { name = 'AddonModGlossaryEntryLinkHandler'; featureName = 'CoreCourseModuleDelegate_AddonModGlossary'; - pattern = /\/mod\/glossary\/showentry\.php.*([\&\?]eid=\d+)/; + pattern = /\/mod\/glossary\/(showentry|view)\.php.*([\&\?](eid|g|mode|hook)=\d+)/; constructor( private domUtils: CoreDomUtilsProvider, @@ -51,7 +51,13 @@ export class AddonModGlossaryEntryLinkHandler extends CoreContentLinksHandlerBas return [{ action: (siteId, navCtrl?): void => { const modal = this.domUtils.showModalLoading(); - const entryId = parseInt(params.eid, 10); + let entryId; + if (params.mode == 'entry') { + entryId = parseInt(params.hook, 10); + } else { + entryId = parseInt(params.eid, 10); + } + let promise; if (courseId) { From 0724235a06d804001a3e294e42a0c313efab166e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 9 Aug 2019 14:59:49 +0200 Subject: [PATCH 171/241] MOBILE-3025 blocks: Uniform tags styling --- src/addon/block/blogtags/blogtags.scss | 122 +++++++++-------- src/addon/block/tags/tags.scss | 176 +++++++++++++------------ 2 files changed, 155 insertions(+), 143 deletions(-) diff --git a/src/addon/block/blogtags/blogtags.scss b/src/addon/block/blogtags/blogtags.scss index 859b876da81..a974b45cbd0 100644 --- a/src/addon/block/blogtags/blogtags.scss +++ b/src/addon/block/blogtags/blogtags.scss @@ -6,77 +6,83 @@ -webkit-padding-start: 0; li { - padding: 0 .2em; - display: inline; - } - } - .s20 { - font-size: 1.5em; - font-weight: bold; - } + padding: .2em; + display: inline-block; - .s19 { - font-size: 1.5em; - } + a { + @extend ion-badge; + @extend .badge-md; + text-decoration: none; + } + .s20 { + font-size: 1.5em; + font-weight: bold; + } - .s18 { - font-size: 1.4em; - font-weight: bold; - } + .s19 { + font-size: 1.5em; + } - .s17 { - font-size: 1.4em; - } + .s18 { + font-size: 1.4em; + font-weight: bold; + } - .s16 { - font-size: 1.3em; - font-weight: bold; - } + .s17 { + font-size: 1.4em; + } - .s15 { - font-size: 1.3em; - } + .s16 { + font-size: 1.3em; + font-weight: bold; + } - .s14 { - font-size: 1.2em; - font-weight: bold; - } + .s15 { + font-size: 1.3em; + } - .s13 { - font-size: 1.2em; - } + .s14 { + font-size: 1.2em; + font-weight: bold; + } - .s12, - .s11 { - font-size: 1.1em; - font-weight: bold; - } + .s13 { + font-size: 1.2em; + } - .s10, - .s9 { - font-size: 1.1em; - } + .s12, + .s11 { + font-size: 1.1em; + font-weight: bold; + } - .s8, - .s7 { - font-size: 1em; - font-weight: bold; - } + .s10, + .s9 { + font-size: 1.1em; + } - .s6, - .s5 { - font-size: 1em; - } + .s8, + .s7 { + font-size: 1em; + font-weight: bold; + } - .s4, - .s3 { - font-size: 0.9em; - font-weight: bold; - } + .s6, + .s5 { + font-size: 1em; + } - .s2, - .s1 { - font-size: 0.9em; + .s4, + .s3 { + font-size: 0.9em; + font-weight: bold; + } + + .s2, + .s1 { + font-size: 0.9em; + } + } } } } \ No newline at end of file diff --git a/src/addon/block/tags/tags.scss b/src/addon/block/tags/tags.scss index cd3df32ebd0..f4c54d1673c 100644 --- a/src/addon/block/tags/tags.scss +++ b/src/addon/block/tags/tags.scss @@ -8,93 +8,99 @@ -webkit-padding-start: 0; li { - padding: 0 .2em; - display: inline; + padding: .2em; + display: inline-block; + + a { + @extend ion-badge; + @extend .badge-md; + text-decoration: none; + } + .s20 { + font-size: 2.7em; + } + + .s19 { + font-size: 2.6em; + } + + .s18 { + font-size: 2.5em; + } + + .s17 { + font-size: 2.4em; + } + + .s16 { + font-size: 2.3em; + } + + .s15 { + font-size: 2.2em; + } + + .s14 { + font-size: 2.1em; + } + + .s13 { + font-size: 2em; + } + + .s12 { + font-size: 1.9em; + } + + .s11 { + font-size: 1.8em; + } + + .s10 { + font-size: 1.7em; + } + + .s9 { + font-size: 1.6em; + } + + .s8 { + font-size: 1.5em; + } + + .s7 { + font-size: 1.4em; + } + + .s6 { + font-size: 1.3em; + } + + .s5 { + font-size: 1.2em; + } + + .s4 { + font-size: 1.1em; + } + + .s3 { + font-size: 1em; + } + + .s2 { + font-size: 0.9em; + } + + .s1 { + font-size: 0.8em; + } + + .s0 { + font-size: 0.7em; + } } } } - .tag_cloud .s20 { - font-size: 2.7em; - } - - .tag_cloud .s19 { - font-size: 2.6em; - } - - .tag_cloud .s18 { - font-size: 2.5em; - } - - .tag_cloud .s17 { - font-size: 2.4em; - } - - .tag_cloud .s16 { - font-size: 2.3em; - } - - .tag_cloud .s15 { - font-size: 2.2em; - } - - .tag_cloud .s14 { - font-size: 2.1em; - } - - .tag_cloud .s13 { - font-size: 2em; - } - - .tag_cloud .s12 { - font-size: 1.9em; - } - - .tag_cloud .s11 { - font-size: 1.8em; - } - - .tag_cloud .s10 { - font-size: 1.7em; - } - - .tag_cloud .s9 { - font-size: 1.6em; - } - - .tag_cloud .s8 { - font-size: 1.5em; - } - - .tag_cloud .s7 { - font-size: 1.4em; - } - - .tag_cloud .s6 { - font-size: 1.3em; - } - - .tag_cloud .s5 { - font-size: 1.2em; - } - - .tag_cloud .s4 { - font-size: 1.1em; - } - - .tag_cloud .s3 { - font-size: 1em; - } - - .tag_cloud .s2 { - font-size: 0.9em; - } - - .tag_cloud .s1 { - font-size: 0.8em; - } - - .tag_cloud .s0 { - font-size: 0.7em; - } } } \ No newline at end of file From 0e9d0356af1f3ca50ed10782d1e7083a11924139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 9 Aug 2019 15:00:09 +0200 Subject: [PATCH 172/241] MOBILE-3025 blocks: Fix comments block link --- src/addon/block/comments/providers/block-handler.ts | 2 +- src/core/comments/pages/viewer/viewer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/addon/block/comments/providers/block-handler.ts b/src/addon/block/comments/providers/block-handler.ts index 81e5b2c15e7..ada6e16549d 100644 --- a/src/addon/block/comments/providers/block-handler.ts +++ b/src/addon/block/comments/providers/block-handler.ts @@ -47,7 +47,7 @@ export class AddonBlockCommentsHandler extends CoreBlockBaseHandler { component: CoreBlockOnlyTitleComponent, link: 'CoreCommentsViewerPage', linkParams: { contextLevel: contextLevel, instanceId: instanceId, - component: 'block_comments', area: 'page_comments', itemId: 0 } + componentName: 'block_comments', area: 'page_comments', itemId: 0 } }; } } diff --git a/src/core/comments/pages/viewer/viewer.ts b/src/core/comments/pages/viewer/viewer.ts index 0f606feee35..e84d27e0c73 100644 --- a/src/core/comments/pages/viewer/viewer.ts +++ b/src/core/comments/pages/viewer/viewer.ts @@ -156,7 +156,7 @@ export class CoreCommentsViewerPage implements OnDestroy { this.canAddComments = this.addDeleteCommentsAvailable && response.canpost; const comments = response.comments.sort((a, b) => b.timecreated - a.timecreated); - this.canLoadMore = comments.length >= CoreCommentsProvider.pageSize; + this.canLoadMore = comments.length > 0 && comments.length >= CoreCommentsProvider.pageSize; return Promise.all(comments.map((comment) => { // Get the user profile image. From 6c0594431db94803cc10cab7773c9265523487ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 12 Aug 2019 12:45:20 +0200 Subject: [PATCH 173/241] MOBILE-1927 calendar: Fix error on description sync --- src/addon/calendar/providers/calendar-sync.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/addon/calendar/providers/calendar-sync.ts b/src/addon/calendar/providers/calendar-sync.ts index 6b81d39ae81..457c25bb3ee 100644 --- a/src/addon/calendar/providers/calendar-sync.ts +++ b/src/addon/calendar/providers/calendar-sync.ts @@ -248,6 +248,11 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { // Try to send the data. const data = this.utils.clone(event); // Clone the object because it will be modified in the submit function. + data.description = { + text: data.description, + format: 1 + }; + return this.calendarProvider.submitEventOnline(eventId > 0 ? eventId : undefined, data, siteId).then((newEvent) => { result.updated = true; result.events.push(newEvent); From 2aa37308ef056f53a4512fca994acc003cd4c70f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 12 Aug 2019 15:30:43 +0200 Subject: [PATCH 174/241] MOBILE-3074 format-text: Add magnifying glass when no height --- src/app/app.scss | 1 - src/directives/format-text.ts | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/app.scss b/src/app/app.scss index f2e7d58928b..8bbc879591f 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -226,7 +226,6 @@ ion-app.app-root { user-select: text; word-break: break-word; word-wrap: break-word; - white-space: normal; &[maxHeight], &[ng-reflect-max-height] { diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index d9977a38836..ada6cea1992 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -160,6 +160,7 @@ export class CoreFormatTextDirective implements OnChanges { */ addMagnifyingGlasses(): void { const imgs = Array.from(this.element.querySelectorAll('.core-adapted-img-container > img')); + console.error(this.element, imgs); if (!imgs.length) { return; } @@ -198,6 +199,7 @@ export class CoreFormatTextDirective implements OnChanges { this.domUtils.viewImage(imgSrc, img.getAttribute('alt'), this.component, this.componentId); }); + console.error(img.parentNode, anchor); img.parentNode.appendChild(anchor); }); } @@ -339,6 +341,9 @@ export class CoreFormatTextDirective implements OnChanges { } } else { this.domUtils.moveChildren(div, this.element); + + // Add magnifying glasses to images. + this.addMagnifyingGlasses(); } this.element.classList.remove('core-disable-media-adapt'); From 88db3d5816c1c4f5db88d509f502f1935d92e098 Mon Sep 17 00:00:00 2001 From: Mark Johnson Date: Fri, 2 Aug 2019 15:53:08 +0100 Subject: [PATCH 175/241] MOBILE-3113 course formats: Give the user the option to reload if format plugins fail to initialise --- src/core/course/providers/course.ts | 7 ++++++- src/core/courses/lang/en.json | 6 ++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index f812ef2b6c0..d1579357467 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -981,7 +981,12 @@ export class CoreCourseProvider { } }).catch(() => { // The site plugin failed to load. The user needs to restart the app to try loading it again. - this.domUtils.showErrorModal('core.courses.errorloadplugins', true); + const message = this.translate.instant('core.courses.errorloadplugins'); + const reload = this.translate.instant('core.courses.reload'); + const ignore = this.translate.instant('core.courses.ignore'); + this.domUtils.showConfirm(message, '', reload, ignore).then(() => { + window.location.reload(); + }); }); } else { // No custom format plugin. We don't need to wait for anything. diff --git a/src/core/courses/lang/en.json b/src/core/courses/lang/en.json index 9409f2ee071..ef69785f5d7 100644 --- a/src/core/courses/lang/en.json +++ b/src/core/courses/lang/en.json @@ -11,11 +11,12 @@ "enrolme": "Enrol me", "errorloadcategories": "An error occurred while loading categories.", "errorloadcourses": "An error occurred while loading courses.", - "errorloadplugins": "The plugins required by this course could not be loaded correctly. Please restart the app to try again.", + "errorloadplugins": "The plugins required by this course could not be loaded correctly. Please reload the app to try again.", "errorsearching": "An error occurred while searching.", "errorselfenrol": "An error occurred while self enrolling.", "filtermycourses": "Filter my courses", "frontpage": "Front page", + "ignore": "Ignore", "hidecourse": "Hide from view", "mycourses": "My courses", "nocourses": "No course information to show.", @@ -26,6 +27,7 @@ "password": "Enrolment key", "paymentrequired": "This course requires a payment for entry.", "paypalaccepted": "PayPal payments accepted", + "reload": "Reload", "removefromfavourites": "Unstar this course", "search": "Search", "searchcourses": "Search courses", @@ -34,4 +36,4 @@ "sendpaymentbutton": "Send payment via PayPal", "show": "Show this course", "totalcoursesearchresults": "Total courses: {{$a}}" -} \ No newline at end of file +} From c07e948e58f96857f44d6a8d801049f42a420b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 12 Aug 2019 16:14:33 +0200 Subject: [PATCH 176/241] MOBILE-3113 lang: Add new strings to index --- scripts/langindex.json | 2 ++ src/assets/lang/en.json | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index a8e23425baa..454c21834e8 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1413,6 +1413,7 @@ "core.courses.filtermycourses": "local_moodlemobileapp", "core.courses.frontpage": "admin", "core.courses.hidecourse": "block_myoverview", + "core.courses.ignore": "admin", "core.courses.mycourses": "moodle", "core.courses.mymoodle": "admin", "core.courses.nocourses": "my", @@ -1423,6 +1424,7 @@ "core.courses.password": "local_moodlemobileapp", "core.courses.paymentrequired": "moodle", "core.courses.paypalaccepted": "enrol_paypal", + "core.courses.reload": "moodle", "core.courses.removefromfavourites": "block_myoverview", "core.courses.search": "moodle", "core.courses.searchcourses": "moodle", diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 6d03d0b2252..54e78480ce7 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1403,12 +1403,13 @@ "core.courses.enrolme": "Enrol me", "core.courses.errorloadcategories": "An error occurred while loading categories.", "core.courses.errorloadcourses": "An error occurred while loading courses.", - "core.courses.errorloadplugins": "The plugins required by this course could not be loaded correctly. Please restart the app to try again.", + "core.courses.errorloadplugins": "The plugins required by this course could not be loaded correctly. Please reload the app to try again.", "core.courses.errorsearching": "An error occurred while searching.", "core.courses.errorselfenrol": "An error occurred while self enrolling.", "core.courses.filtermycourses": "Filter my courses", "core.courses.frontpage": "Front page", "core.courses.hidecourse": "Hide from view", + "core.courses.ignore": "Ignore", "core.courses.mycourses": "My courses", "core.courses.mymoodle": "Dashboard", "core.courses.nocourses": "No course information to show.", @@ -1419,6 +1420,7 @@ "core.courses.password": "Enrolment key", "core.courses.paymentrequired": "This course requires a payment for entry.", "core.courses.paypalaccepted": "PayPal payments accepted", + "core.courses.reload": "Reload", "core.courses.removefromfavourites": "Unstar this course", "core.courses.search": "Search", "core.courses.searchcourses": "Search courses", From dcec83759fa91b59fa3ddbc46b95f5aa32f34195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 13 Aug 2019 13:24:31 +0200 Subject: [PATCH 177/241] MOBILE-3104 calendar: Fix calendar month mini param --- src/addon/calendar/pages/index/index.scss | 3 +++ src/addon/calendar/providers/calendar.ts | 9 +++++++-- src/directives/format-text.ts | 2 -- 3 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 src/addon/calendar/pages/index/index.scss diff --git a/src/addon/calendar/pages/index/index.scss b/src/addon/calendar/pages/index/index.scss new file mode 100644 index 00000000000..213b8abe5bc --- /dev/null +++ b/src/addon/calendar/pages/index/index.scss @@ -0,0 +1,3 @@ +page-addon-calendar-index .toolbar-ios ion-title { + @include padding-horizontal(null, 120px); +} \ No newline at end of file diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 7b09df4a852..17f372590d4 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -1176,10 +1176,15 @@ export class AddonCalendarProvider { const data: any = { year: year, - month: month, - mini: 1 // Set mini to 1 to prevent returning the course selector HTML. + month: month }; + // This parameter requires Moodle 3.5. + if ( site.isVersionGreaterEqualThan('3.5')) { + // Set mini to 1 to prevent returning the course selector HTML. + data.mini = 1; + } + if (courseId) { data.courseid = courseId; } diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index ada6cea1992..ea7b02b9a02 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -160,7 +160,6 @@ export class CoreFormatTextDirective implements OnChanges { */ addMagnifyingGlasses(): void { const imgs = Array.from(this.element.querySelectorAll('.core-adapted-img-container > img')); - console.error(this.element, imgs); if (!imgs.length) { return; } @@ -199,7 +198,6 @@ export class CoreFormatTextDirective implements OnChanges { this.domUtils.viewImage(imgSrc, img.getAttribute('alt'), this.component, this.componentId); }); - console.error(img.parentNode, anchor); img.parentNode.appendChild(anchor); }); } From b304bdfc3e9ca606fc215c63d2a3478ad27acff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 14 Aug 2019 11:24:41 +0200 Subject: [PATCH 178/241] MOBILE-1927 calendar: Fix timezone when adding events --- src/addon/calendar/pages/edit-event/edit-event.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index f2808c6277d..2daaf90cf27 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -423,7 +423,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { submit(): void { // Validate data. const formData = this.eventForm.value, - timeStartDate = this.timeUtils.datetimeToDate(formData.timestart), + timeStartDate = new Date(formData.timestart), timeUntilDate = this.timeUtils.datetimeToDate(formData.timedurationuntil), timeDurationMinutes = parseInt(formData.timedurationminutes || '', 10); let error; From 4d4c5da81e0f782fb7b75c013ac139abb64b3fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 16 Aug 2019 12:12:17 +0200 Subject: [PATCH 179/241] MOBILE-1927 calendar: Fix timezone when adding events --- .../calendar/pages/edit-event/edit-event.ts | 10 ++++---- .../datetime/providers/handler.ts | 7 +++--- src/components/ion-tabs/ion-tabs.scss | 1 - src/providers/utils/time.ts | 23 +++++-------------- 4 files changed, 14 insertions(+), 27 deletions(-) diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index 2daaf90cf27..99b887ac871 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -423,8 +423,8 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { submit(): void { // Validate data. const formData = this.eventForm.value, - timeStartDate = new Date(formData.timestart), - timeUntilDate = this.timeUtils.datetimeToDate(formData.timedurationuntil), + timeStartDate = this.timeUtils.convertToTimestamp(formData.timestart), + timeUntilDate = this.timeUtils.convertToTimestamp(formData.timedurationuntil), timeDurationMinutes = parseInt(formData.timedurationminutes || '', 10); let error; @@ -436,7 +436,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { error = 'core.selectagroup'; } else if (formData.eventtype == AddonCalendarProvider.TYPE_CATEGORY && !formData.categoryid) { error = 'core.selectacategory'; - } else if (formData.duration == 1 && timeStartDate.getTime() > timeUntilDate.getTime()) { + } else if (formData.duration == 1 && timeStartDate > timeUntilDate) { error = 'addon.calendar.invalidtimedurationuntil'; } else if (formData.duration == 2 && (isNaN(timeDurationMinutes) || timeDurationMinutes < 1)) { error = 'addon.calendar.invalidtimedurationminutes'; @@ -453,7 +453,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { const data: any = { name: formData.name, eventtype: formData.eventtype, - timestart: Math.floor(timeStartDate.getTime() / 1000), + timestart: timeStartDate, description: { text: formData.description, format: 1 @@ -473,7 +473,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { } if (formData.duration == 1) { - data.timedurationuntil = Math.floor(timeUntilDate.getTime() / 1000); + data.timedurationuntil = timeUntilDate; } else if (formData.duration == 2) { data.timedurationminutes = formData.timedurationminutes; } diff --git a/src/addon/userprofilefield/datetime/providers/handler.ts b/src/addon/userprofilefield/datetime/providers/handler.ts index 65a7a6f6689..29d3d321ece 100644 --- a/src/addon/userprofilefield/datetime/providers/handler.ts +++ b/src/addon/userprofilefield/datetime/providers/handler.ts @@ -14,6 +14,7 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@core/user/providers/user-profile-field-delegate'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { AddonUserProfileFieldDatetimeComponent } from '../component/datetime'; /** @@ -24,7 +25,7 @@ export class AddonUserProfileFieldDatetimeHandler implements CoreUserProfileFiel name = 'AddonUserProfileFieldDatetime'; type = 'datetime'; - constructor() { + constructor(protected timeUtils: CoreTimeUtilsProvider) { // Nothing to do. } @@ -50,12 +51,10 @@ export class AddonUserProfileFieldDatetimeHandler implements CoreUserProfileFiel const name = 'profile_field_' + field.shortname; if (formValues[name]) { - const milliseconds = new Date(formValues[name]).getTime(); - return { type: 'datetime', name: 'profile_field_' + field.shortname, - value: Math.round(milliseconds / 1000) + value: this.timeUtils.convertToTimestamp(formValues[name]) }; } } diff --git a/src/components/ion-tabs/ion-tabs.scss b/src/components/ion-tabs/ion-tabs.scss index 6b16d9401bf..31f66765086 100644 --- a/src/components/ion-tabs/ion-tabs.scss +++ b/src/components/ion-tabs/ion-tabs.scss @@ -3,7 +3,6 @@ $core-sidetab-size: 60px !default; ion-app.app-root core-ion-tabs { .tabbar { z-index: 101; // For some reason, the regular z-index isn't enough with our tabs, use a higher one. - height: 56px; .core-ion-tabs-loading { height: 100%; diff --git a/src/providers/utils/time.ts b/src/providers/utils/time.ts index 6e2e00aa7ce..9be16dd5865 100644 --- a/src/providers/utils/time.ts +++ b/src/providers/utils/time.ts @@ -308,22 +308,7 @@ export class CoreTimeUtilsProvider { toDatetimeFormat(timestamp?: number): string { timestamp = timestamp || Date.now(); - return this.userDate(timestamp, 'YYYY-MM-DDTHH:mm:ss.SSS', false); - } - - /** - * Convert the value of a ion-datetime to a Date. - * - * @param {string} value Value of ion-datetime. - * @return {Date} Date. - */ - datetimeToDate(value: string): Date { - if (typeof value == 'string' && value.slice(-1) == 'Z') { - // The value shoudln't have the timezone because it causes problems, remove it. - value = value.substr(0, value.length - 1); - } - - return new Date(value); + return this.userDate(timestamp, 'YYYY-MM-DDTHH:mm:ss.SSS', false) + 'Z'; } /** @@ -333,7 +318,11 @@ export class CoreTimeUtilsProvider { * @return {number} Converted timestamp. */ convertToTimestamp(date: string): number { - return moment(date).unix() - (moment().utcOffset() * 60); + if (typeof date == 'string' && date.slice(-1) == 'Z') { + return moment(date).unix() - (moment().utcOffset() * 60); + } + + return moment(date).unix(); } /** From b66991f777fcae821a7ecae435fb70658989d37d Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 19 Aug 2019 15:59:48 +0200 Subject: [PATCH 180/241] MOBILE-3090 calendar: Improve invalidate data after sync --- .../calendar/pages/edit-event/edit-event.ts | 23 ++-- src/addon/calendar/pages/event/event.ts | 17 ++- .../calendar/providers/calendar-offline.ts | 2 +- src/addon/calendar/providers/calendar-sync.ts | 33 +++-- src/addon/calendar/providers/helper.ts | 119 +++++++++++------- 5 files changed, 127 insertions(+), 67 deletions(-) diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index 2daaf90cf27..91f8a505a79 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -479,7 +479,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { } if (formData.repeat) { - data.repeats = formData.repeats; + data.repeats = Number(formData.repeats); } if (this.event && this.event.repeatid) { @@ -489,15 +489,22 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { // Send the data. const modal = this.domUtils.showModalLoading('core.sending', true); + let event; this.calendarProvider.submitEvent(this.eventId, data).then((result) => { - const numberOfRepetitions = formData.repeat ? formData.repeats : - (data.repeateditall && this.event.othereventscount ? this.event.othereventscount + 1 : 1); - this.calendarHelper.invalidateRepeatedEventsOnCalendar(result.event, numberOfRepetitions).catch(() => { - // Ignore errors. - }).then(() => { - this.returnToList(result.event); - }); + event = result.event; + + if (result.sent) { + // Event created or edited, invalidate right days & months. + const numberOfRepetitions = formData.repeat ? formData.repeats : + (data.repeateditall && this.event.othereventscount ? this.event.othereventscount + 1 : 1); + + this.calendarHelper.invalidateRepeatedEventsOnCalendarForEvent(result.event, numberOfRepetitions).catch(() => { + // Ignore errors. + }); + } + }).then(() => { + this.returnToList(event); }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'Error sending data.'); }).finally(() => { diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index 2d481122772..d23579df07f 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -445,10 +445,19 @@ export class AddonCalendarEventPage implements OnDestroy { const modal = this.domUtils.showModalLoading('core.sending', true); this.calendarProvider.deleteEvent(this.event.id, this.event.name, deleteAll).then((sent) => { - this.calendarHelper.invalidateRepeatedEventsOnCalendar(this.event, deleteAll ? this.event.eventcount : 1) - .catch(() => { - // Ignore errors. - }).then(() => { + let promise; + + if (sent) { + // Event deleted, invalidate right days & months. + promise = this.calendarHelper.invalidateRepeatedEventsOnCalendarForEvent(this.event, + deleteAll ? this.event.eventcount : 1).catch(() => { + // Ignore errors. + }); + } else { + promise = Promise.resolve(); + } + + return promise.then(() => { // Trigger an event. this.eventsProvider.trigger(AddonCalendarProvider.DELETED_EVENT_EVENT, { eventId: this.eventId, diff --git a/src/addon/calendar/providers/calendar-offline.ts b/src/addon/calendar/providers/calendar-offline.ts index ee06f75c98f..5b0f024362a 100644 --- a/src/addon/calendar/providers/calendar-offline.ts +++ b/src/addon/calendar/providers/calendar-offline.ts @@ -95,7 +95,7 @@ export class AddonCalendarOfflineProvider { }, { name: 'repeats', - type: 'TEXT', + type: 'INTEGER', }, { name: 'repeatid', diff --git a/src/addon/calendar/providers/calendar-sync.ts b/src/addon/calendar/providers/calendar-sync.ts index 457c25bb3ee..b395ae6af19 100644 --- a/src/addon/calendar/providers/calendar-sync.ts +++ b/src/addon/calendar/providers/calendar-sync.ts @@ -26,6 +26,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { AddonCalendarProvider } from './calendar'; import { AddonCalendarOfflineProvider } from './calendar-offline'; +import { AddonCalendarHelperProvider } from './helper'; /** * Service to sync calendar. @@ -48,7 +49,8 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { timeUtils: CoreTimeUtilsProvider, private utils: CoreUtilsProvider, private calendarProvider: AddonCalendarProvider, - private calendarOffline: AddonCalendarOfflineProvider) { + private calendarOffline: AddonCalendarOfflineProvider, + private calendarHelper: AddonCalendarHelperProvider) { super('AddonCalendarSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils); @@ -124,6 +126,7 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { warnings: [], events: [], deleted: [], + toinvalidate: [], updated: false }; let offlineEventIds: number[]; @@ -152,18 +155,13 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { return this.utils.allPromises(promises); }).then(() => { if (result.updated) { + // Data has been sent to server. Now invalidate the WS calls. const promises = [ this.calendarProvider.invalidateEventsList(siteId), + this.calendarHelper.invalidateRepeatedEventsOnCalendar(result.toinvalidate, siteId) ]; - offlineEventIds.forEach((eventId) => { - if (eventId > 0) { - // An event was edited, invalidate its data too. - promises.push(this.calendarProvider.invalidateEvent(eventId, siteId)); - } - }); - return Promise.all(promises).catch(() => { // Ignore errors. }); @@ -214,6 +212,16 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { // Ignore errors, maybe there was no edit data. })); + // We need the event data to invalidate it. Get it from local DB. + promises.push(this.calendarProvider.getEventFromLocalDb(eventId, siteId).then((event) => { + result.toinvalidate.push({ + event: event, + repeated: data.repeat ? event.eventcount : 1 + }); + }).catch(() => { + // Ignore errors. + })); + return Promise.all(promises); }).catch((error) => { @@ -257,6 +265,15 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { result.updated = true; result.events.push(newEvent); + // Add data to invalidate. + const numberOfRepetitions = data.repeat ? data.repeats : + (data.repeateditall && newEvent.repeatid ? newEvent.eventcount : 1); + + result.toinvalidate.push({ + event: newEvent, + repeated: numberOfRepetitions + }); + // Event sent, delete the offline data. return this.calendarOffline.deleteEvent(event.id, siteId); }).catch((error) => { diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index c3b24c9569a..36d1f651856 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -344,73 +344,100 @@ export class AddonCalendarHelperProvider { /** * Invalidate all calls from calendar WS calls. * - * @param {any} event Event that has been touched. - * @param {number} repeated Number of times the event is repeated. + * @param {{event: any, repeated: number}[]} events Events that have been touched and number of times each event is repeated. * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} REsolved when done. + * @return {Promise} Resolved when done. */ - invalidateRepeatedEventsOnCalendar(event: any, repeated: number, siteId?: string): Promise { + invalidateRepeatedEventsOnCalendar(events: {event: any, repeated: number}[], siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - let invalidatePromise; const timestarts = []; - if (repeated > 1) { - if (event.repeatid) { - // Being edited or deleted. - invalidatePromise = this.calendarProvider.getLocalEventsByRepeatIdFromLocalDb(event.repeatid, site.id) - .then((events) => { - return this.utils.allPromises(events.map((event) => { - timestarts.push(event.timestart); - - return this.calendarProvider.invalidateEvent(event.id); - })); - }); - } else { - // Being added. - let time = event.timestart; - while (repeated > 0) { - timestarts.push(time); - time += CoreConstants.SECONDS_DAY * 7; - repeated--; + // Invalidate the events and get the timestarts so we can invalidate months & days. + return this.utils.allPromises(events.map((eventData) => { + + if (eventData.repeated > 1) { + if (eventData.event.repeatid) { + // Being edited or deleted. + // We need to calculate the days to invalidate because the event date could have changed. + // We don't know if the repeated events are before or after this one, invalidate them all. + timestarts.push(eventData.event.timestart); + + for (let i = 1; i < eventData.repeated; i++) { + timestarts.push(eventData.event.timestart + CoreConstants.SECONDS_DAY * 7 * i); + timestarts.push(eventData.event.timestart - CoreConstants.SECONDS_DAY * 7 * i); + } + + // Get the repeated events to invalidate them. + return this.calendarProvider.getLocalEventsByRepeatIdFromLocalDb(eventData.event.repeatid, site.id) + .then((events) => { + + return this.utils.allPromises(events.map((event) => { + return this.calendarProvider.invalidateEvent(event.id); + })); + }); + } else { + // Being added. + let time = eventData.event.timestart; + while (eventData.repeated > 0) { + timestarts.push(time); + time += CoreConstants.SECONDS_DAY * 7; + eventData.repeated--; + } + + return Promise.resolve(); } + } else { + // Not repeated. + timestarts.push(eventData.event.timestart); - invalidatePromise = Promise.resolve(); + return this.calendarProvider.invalidateEvent(eventData.event.id); } - } else { - // Not repeated. - timestarts.push(event.timestart); - invalidatePromise = this.calendarProvider.invalidateEvent(event.id); - } - return invalidatePromise.finally(() => { - let lastMonth, lastYear; + })).finally(() => { + const invalidatedMonths = {}, + invalidatedDays = {}; return this.utils.allPromises([ this.calendarProvider.invalidateAllUpcomingEvents(), - // Invalidate months. + // Invalidate months and days. this.utils.allPromises(timestarts.map((time) => { - const day = moment(new Date(time * 1000)); + const promises = [], + day = moment(new Date(time * 1000)), + monthId = this.getMonthId(day.year(), day.month() + 1), + dayId = monthId + '#' + day.date(); - if (lastMonth && (lastMonth == day.month() + 1 && lastYear == day.year())) { - return Promise.resolve(); - } + if (!invalidatedMonths[monthId]) { + // Month not invalidated already, do it now. + invalidatedMonths[monthId] = monthId; - // Invalidate once. - lastMonth = day.month() + 1; - lastYear = day.year(); + promises.push(this.calendarProvider.invalidateMonthlyEvents(day.year(), day.month() + 1, site.id)); + } - return this.calendarProvider.invalidateMonthlyEvents(lastYear, lastMonth, site.id); - })), + if (!invalidatedDays[dayId]) { + // Day not invalidated already, do it now. + invalidatedDays[dayId] = dayId; - // Invalidate days. - this.utils.allPromises(timestarts.map((time) => { - const day = moment(new Date(time * 1000)); + promises.push(this.calendarProvider.invalidateDayEvents(day.year(), day.month() + 1, day.date(), + site.id)); + } - return this.calendarProvider.invalidateDayEvents(day.year(), day.month() + 1, day.date(), site.id); - })), + return this.utils.allPromises(promises); + })) ]); }); }); } + + /** + * Invalidate all calls from calendar WS calls. + * + * @param {any} event Event that has been touched. + * @param {number} repeated Number of times the event is repeated. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when done. + */ + invalidateRepeatedEventsOnCalendarForEvent(event: any, repeated: number, siteId?: string): Promise { + return this.invalidateRepeatedEventsOnCalendar([{event: event, repeated: repeated}], siteId); + } } From ada49974f4b635e611dfb22a1fc28c8495d73a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 20 Aug 2019 09:27:27 +0200 Subject: [PATCH 181/241] MOBILE-3117 login: Check you entered the credentials page again --- src/core/login/pages/credentials/credentials.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/core/login/pages/credentials/credentials.ts b/src/core/login/pages/credentials/credentials.ts index eb791bab415..fbf6eb57475 100644 --- a/src/core/login/pages/credentials/credentials.ts +++ b/src/core/login/pages/credentials/credentials.ts @@ -81,6 +81,13 @@ export class CoreLoginCredentialsPage { } } + /** + * View enter. + */ + ionViewDidEnter(): void { + this.viewLeft = false; + } + /** * View left. */ From a347a6d4ed9adec6336c296b13b59a2ee41f3bfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 20 Aug 2019 09:35:00 +0200 Subject: [PATCH 182/241] MOBILE-3117 login: Fix logout on reconnect --- src/app/app.component.ts | 5 +++- src/core/login/pages/reconnect/reconnect.html | 2 +- src/core/login/pages/reconnect/reconnect.ts | 15 ++++++---- src/providers/sites.ts | 29 ++++++++----------- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 1be81e471a8..cd0200f8d2d 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -25,6 +25,7 @@ import { CoreCustomURLSchemesProvider } from '@providers/urlschemes'; import { CoreLoginHelperProvider } from '@core/login/providers/helper'; import { Keyboard } from '@ionic-native/keyboard'; import { ScreenOrientation } from '@ionic-native/screen-orientation'; +import { CoreLoginSitesPage } from '@core/login/pages/sites/sites'; @Component({ templateUrl: 'app.html' @@ -74,7 +75,9 @@ export class MoodleMobileApp implements OnInit { ngOnInit(): void { this.eventsProvider.on(CoreEventsProvider.LOGOUT, () => { // Go to sites page when user is logged out. - this.appProvider.getRootNavController().setRoot('CoreLoginSitesPage'); + // Due to DeepLinker, we need to use the ViewCtrl instead of name. + // Otherwise some pages are re-created when they shouldn't. + this.appProvider.getRootNavController().setRoot(CoreLoginSitesPage); // Unload lang custom strings. this.langProvider.clearCustomStrings(); diff --git a/src/core/login/pages/reconnect/reconnect.html b/src/core/login/pages/reconnect/reconnect.html index 7d4cadb3711..44955e85aff 100644 --- a/src/core/login/pages/reconnect/reconnect.html +++ b/src/core/login/pages/reconnect/reconnect.html @@ -39,7 +39,7 @@ -
{{ 'core.login.cancel' | translate }} + {{ 'core.login.cancel' | translate }} diff --git a/src/core/login/pages/reconnect/reconnect.ts b/src/core/login/pages/reconnect/reconnect.ts index ba08c11bebb..c653e51a652 100644 --- a/src/core/login/pages/reconnect/reconnect.ts +++ b/src/core/login/pages/reconnect/reconnect.ts @@ -101,13 +101,16 @@ export class CoreLoginReconnectPage { /** * Cancel reconnect. + * + * @param {Event} [e] Event. */ - cancel(): void { - this.sitesProvider.logout().catch(() => { - // Ignore errors (shouldn't happen). - }).finally(() => { - this.navCtrl.setRoot('CoreLoginSitesPage'); - }); + cancel(e?: Event): void { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } + + this.sitesProvider.logout(); } /** diff --git a/src/providers/sites.ts b/src/providers/sites.ts index a80aca76215..c52b205a579 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -27,7 +27,6 @@ import { CoreConfigConstants } from '../configconstants'; import { CoreSite } from '@classes/site'; import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; import { Md5 } from 'ts-md5/dist/md5'; -import { Location } from '@angular/common'; import { WP_PROVIDER } from '@app/app.module'; /** @@ -326,7 +325,7 @@ export class CoreSitesProvider { constructor(logger: CoreLoggerProvider, private http: HttpClient, private sitesFactory: CoreSitesFactoryProvider, private appProvider: CoreAppProvider, private translate: TranslateService, private urlUtils: CoreUrlUtilsProvider, - private eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider, private location: Location, + private eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider, private utils: CoreUtilsProvider, private injector: Injector) { this.logger = logger.getInstance('CoreSitesProvider'); @@ -1221,27 +1220,23 @@ export class CoreSitesProvider { * @return {Promise} Promise resolved when the user is logged out. */ logout(): Promise { - if (!this.currentSite) { - // Already logged out. - return Promise.resolve(); - } + let siteId; + const promises = []; - const siteId = this.currentSite.getId(), - siteConfig = this.currentSite.getStoredConfig(), - promises = []; + if (this.currentSite) { + 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)); - } + if (siteConfig && siteConfig.tool_mobile_forcelogout == '1') { + promises.push(this.setSiteLoggedOut(siteId, true)); + } - promises.push(this.appDB.deleteRecords(this.CURRENT_SITE_TABLE, { id: 1 })); + promises.push(this.appDB.deleteRecords(this.CURRENT_SITE_TABLE, { id: 1 })); + } return Promise.all(promises).finally(() => { - // Due to DeepLinker, we need to remove the path from the URL, otherwise some pages are re-created when they shouldn't. - this.location.replaceState(''); - this.eventsProvider.trigger(CoreEventsProvider.LOGOUT, {}, siteId); }); } From 7fc105dc744e3c0b783abf7e69fdd2e9e744af69 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 20 Aug 2019 12:16:57 +0200 Subject: [PATCH 183/241] MOBILE-3118 siteplugins: Stringify objects copied from otherdata --- src/core/siteplugins/providers/siteplugins.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/core/siteplugins/providers/siteplugins.ts b/src/core/siteplugins/providers/siteplugins.ts index 7fa66171607..fe20b3115e2 100644 --- a/src/core/siteplugins/providers/siteplugins.ts +++ b/src/core/siteplugins/providers/siteplugins.ts @@ -445,12 +445,23 @@ export class CoreSitePluginsProvider { // Include only the properties specified in the array. for (const i in useOtherData) { const name = useOtherData[i]; - args[name] = otherData[name]; + + if (typeof otherData[name] == 'object' && otherData[name] !== null) { + // Stringify objects. + args[name] = JSON.stringify(otherData[name]); + } else { + args[name] = otherData[name]; + } } } else { // Add all the data to args. for (const name in otherData) { - args[name] = otherData[name]; + if (typeof otherData[name] == 'object' && otherData[name] !== null) { + // Stringify objects. + args[name] = JSON.stringify(otherData[name]); + } else { + args[name] = otherData[name]; + } } } From 529cd97cac1b14a1c55c3f1b12039cf1713c2ae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 20 Aug 2019 16:19:34 +0200 Subject: [PATCH 184/241] MOBILE-3068 ios: Style action sheets --- src/app/app.ios.scss | 28 +++++++++++++++++----------- src/app/app.md.scss | 13 ------------- src/app/app.scss | 19 +++++++++++++++++++ src/theme/variables.scss | 11 ++++++++++- 4 files changed, 46 insertions(+), 25 deletions(-) diff --git a/src/app/app.ios.scss b/src/app/app.ios.scss index 2655a22768c..8eeaf67a367 100644 --- a/src/app/app.ios.scss +++ b/src/app/app.ios.scss @@ -99,16 +99,22 @@ ion-app.app-root.ios { background-color: $checkbox-ios-background-color-off; } - // File Uploader - // In iOS the input is 1 level higher, so the styles are different. - .action-sheet-ios input.core-fileuploader-file-handler-input { - position: absolute; - @include position(null, 0, null, 0); - min-width: 100%; - min-height: $action-sheet-ios-button-min-height; - opacity: 0; - outline: none; - z-index: 100; - cursor: pointer; + + .action-sheet-ios { + .action-sheet-title { + font-size: 2rem; + } + // File Uploader + // In iOS the input is 1 level higher, so the styles are different. + input.core-fileuploader-file-handler-input { + position: absolute; + @include position(null, 0, null, 0); + min-width: 100%; + min-height: $action-sheet-ios-button-min-height; + opacity: 0; + outline: none; + z-index: 100; + cursor: pointer; + } } } \ No newline at end of file diff --git a/src/app/app.md.scss b/src/app/app.md.scss index 8a774558556..bee37227555 100644 --- a/src/app/app.md.scss +++ b/src/app/app.md.scss @@ -70,19 +70,6 @@ ion-app.app-root.md { padding-top: 0; margin-top: $action-sheet-md-title-padding-top; } - .action-sheet-cancel { - color: $red; - } - - .action-sheet-wrapper { - bottom: 0; - top: initial; - max-height: 50%; - height: 100%; - } - .action-sheet-selected { - color: $core-color; - } } } diff --git a/src/app/app.scss b/src/app/app.scss index 8bbc879591f..0b52e833700 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -599,6 +599,25 @@ ion-app.app-root { .action-sheet-group { overflow: auto; } + + .action-sheet-wrapper { + .action-sheet-button.action-sheet-cancel { + color: $core-action-sheet-cancel-color; + } + .action-sheet-selected { + color: $core-color; + } + } + + @media (min-height: 500px) { + .action-sheet-wrapper { + bottom: 0; + top: initial; + max-height: 50%; + height: 100%; + } + } + .alert-message { overflow-y: auto; } diff --git a/src/theme/variables.scss b/src/theme/variables.scss index 1727a6d2894..b2ac149d154 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -164,6 +164,9 @@ $core-login-loading-color: false !default; $core-login-item-inner-background-color: $white !default; $core-login-item-background-color: $white !default; +$core-action-sheet-color: $core-color !default; +$core-action-sheet-cancel-color: $danger !default; + // App iOS Variables // -------------------------------------------------- // iOS only Sass variables can go here @@ -182,6 +185,9 @@ $checkbox-ios-disabled-opacity: $input-select-opacity !default; $toggle-ios-disabled-opacity: $input-select-opacity !default; $note-ios-color: $note-color; $popover-ios-width: $popover-width; +$action-sheet-ios-title-color: $core-action-sheet-color; +$action-sheet-ios-button-text-color: $black !default; +$action-sheet-ios-button-destructive-text-color: $danger; $item-ios-divider-background: $item-divider-background; $item-ios-divider-color: $item-divider-color; @@ -204,7 +210,8 @@ $checkbox-md-disabled-opacity: $input-select-opacity !default; $toggle-md-disabled-opacity: $input-select-opacity !default; $note-md-color: $note-color; $popover-md-width: $popover-width; -$action-sheet-md-title-color: $core-color; +$action-sheet-md-title-color: $core-action-sheet-color; +$action-sheet-md-button-text-color: $black !default; $item-md-divider-background: $item-divider-background; $item-md-divider-color: $item-divider-color; @@ -226,6 +233,8 @@ $checkbox-wp-disabled-opacity: $input-select-opacity !default; $toggle-wp-disabled-opacity: $input-select-opacity !default; $note-wp-color: $note-color; $popover-wp-width: $popover-width; +$action-sheet-wp-title-color: $core-action-sheet-color; +$action-sheet-wp-button-text-color: $black !default; $item-wp-divider-background: $item-divider-background; $item-wp-divider-color: $item-divider-color; From 42f44e4945ec0a3cdc5955943230969bf56aa115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 21 Aug 2019 09:48:21 +0200 Subject: [PATCH 185/241] MOBILE-3068 tabs: Fix top tabs flickr --- src/components/tabs/tabs.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index 5d457bfd886..921143228bf 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -417,6 +417,13 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe const scroll = parseInt(scrollElement.scrollTop, 10); if (scroll == this.lastScroll) { + if (scroll == 0) { + // Ensure tabbar is shown. + this.topTabsElement.style.transform = ''; + this.originalTabsContainer.style.transform = ''; + this.originalTabsContainer.style.paddingBottom = this.tabBarHeight + 'px'; + } + // Ensure scroll has been modified to avoid flicks. return; } @@ -438,7 +445,8 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe this.originalTabsContainer.style.transform = 'translateY(-' + scroll + 'px)'; this.originalTabsContainer.style.paddingBottom = this.tabBarHeight - scroll + 'px'; } - this.lastScroll = scroll; + // Use lastScroll after moving the tabs to avoid flickering. + this.lastScroll = parseInt(scrollElement.scrollTop, 10); } /** From 486e1d9b11a342f41193edff511b4d9ea934e0a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 21 Aug 2019 10:01:49 +0200 Subject: [PATCH 186/241] MOBILE-3068 blocks: Fix resize uncaught promise --- src/components/tabs/tab.ts | 2 ++ src/core/block/components/course-blocks/course-blocks.ts | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/components/tabs/tab.ts b/src/components/tabs/tab.ts index 9006b30110f..fc4aaf2fc74 100644 --- a/src/components/tabs/tab.ts +++ b/src/components/tabs/tab.ts @@ -128,6 +128,8 @@ export class CoreTabComponent implements OnInit, OnDestroy { }); this.tabs.showHideTabs(scroll); + }).catch(() => { + // Ignore errors. }); } diff --git a/src/core/block/components/course-blocks/course-blocks.ts b/src/core/block/components/course-blocks/course-blocks.ts index 574dc3c8bcc..7db1ffa8cb8 100644 --- a/src/core/block/components/course-blocks/course-blocks.ts +++ b/src/core/block/components/course-blocks/course-blocks.ts @@ -67,6 +67,10 @@ export class CoreBlockCourseBlocksComponent implements OnInit, OnDestroy { * Setup scrolling. */ protected initScroll(): void { + if (this.blocks.length <= 0) { + return; + } + const scroll: HTMLElement = this.content && this.content.getScrollElement(); this.domUtils.waitElementToExist(() => scroll && scroll.querySelector('.core-course-blocks-side')).then((sideElement) => { @@ -89,6 +93,8 @@ export class CoreBlockCourseBlocksComponent implements OnInit, OnDestroy { this.sideScroll.classList.remove('core-course-blocks-fixed-bottom'); this.scrollWorking = false; } + }).catch(() => { + // Ignore errors. }); } From 9f2bb8c4a719c91ad79bd437236994b007c96c02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 21 Aug 2019 11:11:28 +0200 Subject: [PATCH 187/241] MOBILE-3068 forum: Hide options if forum not loaded --- .../index/addon-mod-forum-index.html | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/addon/mod/forum/components/index/addon-mod-forum-index.html b/src/addon/mod/forum/components/index/addon-mod-forum-index.html index 8a7dae44043..4c8be80bf33 100644 --- a/src/addon/mod/forum/components/index/addon-mod-forum-index.html +++ b/src/addon/mod/forum/components/index/addon-mod-forum-index.html @@ -31,17 +31,17 @@ {{ availabilityMessage }} - - + + + -
- -
+
+ +
- @@ -95,9 +95,9 @@

- - + + From 4ee4692cd67094cb3a1d74f182a653d43ec2d921 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 21 Aug 2019 11:05:10 +0200 Subject: [PATCH 188/241] MOBILE-3106 login: Check redirect if get site info fails --- src/classes/site.ts | 10 +++++++++- src/providers/sites.ts | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/classes/site.ts b/src/classes/site.ts index e01cb15cc56..07bf12cca89 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -1454,7 +1454,15 @@ export class CoreSite { preSets.noLogin = true; preSets.useGet = true; - return this.wsProvider.callAjax('tool_mobile_get_public_config', {}, preSets); + return this.wsProvider.callAjax('tool_mobile_get_public_config', {}, preSets).catch((error2) => { + if (this.getInfo() && this.isVersionGreaterEqualThan('3.8')) { + // GET is supported, return the second error. + return Promise.reject(error2); + } else { + // GET not supported or we don't know if it's supported. Return first error. + return Promise.reject(error); + } + }); } return Promise.reject(error); diff --git a/src/providers/sites.ts b/src/providers/sites.ts index a80aca76215..9588a3e6014 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -470,6 +470,20 @@ export class CoreSitesProvider { // Service supported but an error happened. Return error. error.critical = true; + if (error.errorcode == 'codingerror') { + // This could be caused by a redirect. Check if it's the case. + return this.utils.checkRedirect(siteUrl).then((redirect) => { + if (redirect) { + error.error = this.translate.instant('core.login.sitehasredirect'); + } else { + // We can't be sure if there is a redirect or not. Display cannot connect error. + error.error = this.translate.instant('core.cannotconnect'); + } + + return Promise.reject(error); + }); + } + return Promise.reject(error); } From 78d512f92eac1def0d45bd377388aa4a002a2501 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 21 Aug 2019 16:23:42 +0200 Subject: [PATCH 189/241] MOBILE-3068 course: Fix not enrolled error when prefetch course --- .../coursecompletion/providers/course-option-handler.ts | 6 ++++++ src/addon/coursecompletion/providers/coursecompletion.ts | 1 + src/classes/site.ts | 8 ++++---- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/addon/coursecompletion/providers/course-option-handler.ts b/src/addon/coursecompletion/providers/course-option-handler.ts index d6a08f6e98e..d0483b68a78 100644 --- a/src/addon/coursecompletion/providers/course-option-handler.ts +++ b/src/addon/coursecompletion/providers/course-option-handler.ts @@ -97,6 +97,12 @@ export class AddonCourseCompletionCourseOptionHandler implements CoreCourseOptio return this.courseCompletionProvider.getCompletion(course.id, undefined, { getFromCache: false, emergencyCache: false + }).catch((error) => { + if (error && error.errorcode == 'notenroled') { + // Not enrolled error, probably a teacher. Ignore error. + } else { + return Promise.reject(error); + } }); } } diff --git a/src/addon/coursecompletion/providers/coursecompletion.ts b/src/addon/coursecompletion/providers/coursecompletion.ts index 262bb8e3b9a..0a79a47669f 100644 --- a/src/addon/coursecompletion/providers/coursecompletion.ts +++ b/src/addon/coursecompletion/providers/coursecompletion.ts @@ -110,6 +110,7 @@ export class AddonCourseCompletionProvider { preSets.cacheKey = this.getCompletionCacheKey(courseId, userId); preSets.updateFrequency = preSets.updateFrequency || CoreSite.FREQUENCY_SOMETIMES; + preSets.cacheErrors = ['notenroled']; return site.read('core_completion_get_course_completion_status', data, preSets).then((data) => { if (data.completionstatus) { diff --git a/src/classes/site.ts b/src/classes/site.ts index 07bf12cca89..18be199e145 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -750,15 +750,15 @@ export class CoreSite { // Save the error instead of deleting the cache entry so the same content is displayed in offline. this.saveToCache(method, data, error, preSets); - return Promise.reject(error); - } else if (typeof preSets.emergencyCache !== 'undefined' && !preSets.emergencyCache) { - this.logger.debug(`WS call '${method}' failed. Emergency cache is forbidden, rejecting.`); - return Promise.reject(error); } else if (preSets.cacheErrors && preSets.cacheErrors.indexOf(error.errorcode) != -1) { // Save the error instead of deleting the cache entry so the same content is displayed in offline. this.saveToCache(method, data, error, preSets); + return Promise.reject(error); + } else if (typeof preSets.emergencyCache !== 'undefined' && !preSets.emergencyCache) { + this.logger.debug(`WS call '${method}' failed. Emergency cache is forbidden, rejecting.`); + return Promise.reject(error); } From 6f4101d377d3a625b44eb91e98ff71b030849f34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 21 Aug 2019 16:51:55 +0200 Subject: [PATCH 190/241] MOBILE-3068 imscp: Change toc to a lateral modal --- scripts/langindex.json | 1 + .../mod/imscp/components/components.module.ts | 8 ++--- src/addon/mod/imscp/components/index/index.ts | 27 +++++++++------- .../addon-mod-imscp-toc-popover.html | 5 --- src/addon/mod/imscp/lang/en.json | 3 +- src/addon/mod/imscp/pages/toc/toc.html | 19 ++++++++++++ src/addon/mod/imscp/pages/toc/toc.module.ts | 31 +++++++++++++++++++ .../toc-popover.ts => pages/toc/toc.ts} | 20 +++++++++--- src/assets/lang/en.json | 1 + 9 files changed, 87 insertions(+), 28 deletions(-) delete mode 100644 src/addon/mod/imscp/components/toc-popover/addon-mod-imscp-toc-popover.html create mode 100644 src/addon/mod/imscp/pages/toc/toc.html create mode 100644 src/addon/mod/imscp/pages/toc/toc.module.ts rename src/addon/mod/imscp/{components/toc-popover/toc-popover.ts => pages/toc/toc.ts} (73%) diff --git a/scripts/langindex.json b/scripts/langindex.json index 454c21834e8..8819689b286 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -640,6 +640,7 @@ "addon.mod_imscp.deploymenterror": "imscp", "addon.mod_imscp.modulenameplural": "imscp", "addon.mod_imscp.showmoduledescription": "local_moodlemobileapp", + "addon.mod_imscp.toc": "imscp", "addon.mod_lesson.answer": "lesson", "addon.mod_lesson.attempt": "lesson", "addon.mod_lesson.attemptheader": "lesson", diff --git a/src/addon/mod/imscp/components/components.module.ts b/src/addon/mod/imscp/components/components.module.ts index 259c6c729cd..37a1cbeb251 100644 --- a/src/addon/mod/imscp/components/components.module.ts +++ b/src/addon/mod/imscp/components/components.module.ts @@ -20,12 +20,10 @@ import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreCourseComponentsModule } from '@core/course/components/components.module'; import { AddonModImscpIndexComponent } from './index/index'; -import { AddonModImscpTocPopoverComponent } from './toc-popover/toc-popover'; @NgModule({ declarations: [ AddonModImscpIndexComponent, - AddonModImscpTocPopoverComponent, ], imports: [ CommonModule, @@ -38,12 +36,10 @@ import { AddonModImscpTocPopoverComponent } from './toc-popover/toc-popover'; providers: [ ], exports: [ - AddonModImscpIndexComponent, - AddonModImscpTocPopoverComponent + AddonModImscpIndexComponent ], entryComponents: [ - AddonModImscpIndexComponent, - AddonModImscpTocPopoverComponent + AddonModImscpIndexComponent ] }) export class AddonModImscpComponentsModule {} diff --git a/src/addon/mod/imscp/components/index/index.ts b/src/addon/mod/imscp/components/index/index.ts index 11e53d2b2e2..3b793da191d 100644 --- a/src/addon/mod/imscp/components/index/index.ts +++ b/src/addon/mod/imscp/components/index/index.ts @@ -13,13 +13,12 @@ // limitations under the License. import { Component, Injector } from '@angular/core'; -import { PopoverController } from 'ionic-angular'; +import { ModalController } from 'ionic-angular'; import { CoreAppProvider } from '@providers/app'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; import { AddonModImscpProvider } from '../../providers/imscp'; import { AddonModImscpPrefetchHandler } from '../../providers/prefetch-handler'; -import { AddonModImscpTocPopoverComponent } from '../../components/toc-popover/toc-popover'; /** * Component that displays a IMSCP. @@ -40,7 +39,7 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom nextItem = ''; constructor(injector: Injector, private imscpProvider: AddonModImscpProvider, private courseProvider: CoreCourseProvider, - private appProvider: CoreAppProvider, private popoverCtrl: PopoverController, + private appProvider: CoreAppProvider, private modalCtrl: ModalController, private imscpPrefetch: AddonModImscpPrefetchHandler) { super(injector); } @@ -148,17 +147,23 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom * @param {MouseEvent} event Event. */ showToc(event: MouseEvent): void { - const popover = this.popoverCtrl.create(AddonModImscpTocPopoverComponent, { items: this.items }); - - popover.onDidDismiss((itemId) => { - if (!itemId) { - // Not valid, probably a category. - return; + // Create the toc modal. + const modal = this.modalCtrl.create('AddonModImscpTocPage', { + items: this.items, + selected: this.currentItem + }, { cssClass: 'core-modal-lateral', + showBackdrop: true, + enableBackdropDismiss: true, + enterAnimation: 'core-modal-lateral-transition', + leaveAnimation: 'core-modal-lateral-transition' }); + + modal.onDidDismiss((itemId) => { + if (itemId) { + this.loadItem(itemId); } - this.loadItem(itemId); }); - popover.present({ + modal.present({ ev: event }); } diff --git a/src/addon/mod/imscp/components/toc-popover/addon-mod-imscp-toc-popover.html b/src/addon/mod/imscp/components/toc-popover/addon-mod-imscp-toc-popover.html deleted file mode 100644 index 6bda8d59448..00000000000 --- a/src/addon/mod/imscp/components/toc-popover/addon-mod-imscp-toc-popover.html +++ /dev/null @@ -1,5 +0,0 @@ - - - {{item.title}} - - diff --git a/src/addon/mod/imscp/lang/en.json b/src/addon/mod/imscp/lang/en.json index 4abb95089a5..441eea59866 100644 --- a/src/addon/mod/imscp/lang/en.json +++ b/src/addon/mod/imscp/lang/en.json @@ -1,5 +1,6 @@ { "deploymenterror": "Content package error!", "modulenameplural": "IMS content packages", - "showmoduledescription": "Show description" + "showmoduledescription": "Show description", + "toc": "TOC" } \ No newline at end of file diff --git a/src/addon/mod/imscp/pages/toc/toc.html b/src/addon/mod/imscp/pages/toc/toc.html new file mode 100644 index 00000000000..035a5d6a067 --- /dev/null +++ b/src/addon/mod/imscp/pages/toc/toc.html @@ -0,0 +1,19 @@ + + + {{ 'addon.mod_imscp.toc' | translate }} + + + + + + + + diff --git a/src/addon/mod/imscp/pages/toc/toc.module.ts b/src/addon/mod/imscp/pages/toc/toc.module.ts new file mode 100644 index 00000000000..28f970142eb --- /dev/null +++ b/src/addon/mod/imscp/pages/toc/toc.module.ts @@ -0,0 +1,31 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonModImscpTocPage } from './toc'; + +@NgModule({ + declarations: [ + AddonModImscpTocPage, + ], + imports: [ + CoreDirectivesModule, + IonicPageModule.forChild(AddonModImscpTocPage), + TranslateModule.forChild() + ], +}) +export class AddonModImscpTocPageModule {} diff --git a/src/addon/mod/imscp/components/toc-popover/toc-popover.ts b/src/addon/mod/imscp/pages/toc/toc.ts similarity index 73% rename from src/addon/mod/imscp/components/toc-popover/toc-popover.ts rename to src/addon/mod/imscp/pages/toc/toc.ts index 115c1241f22..1880e4813ec 100644 --- a/src/addon/mod/imscp/components/toc-popover/toc-popover.ts +++ b/src/addon/mod/imscp/pages/toc/toc.ts @@ -13,20 +13,23 @@ // limitations under the License. import { Component } from '@angular/core'; -import { NavParams, ViewController } from 'ionic-angular'; +import { IonicPage, NavParams, ViewController } from 'ionic-angular'; /** - * Component to display the TOC of a IMSCP. + * Modal to display the TOC of a imscp. */ +@IonicPage({ segment: 'addon-mod-imscp-toc-modal' }) @Component({ - selector: 'addon-mod-imscp-toc-popover', - templateUrl: 'addon-mod-imscp-toc-popover.html' + selector: 'page-addon-mod-imscp-toc', + templateUrl: 'toc.html' }) -export class AddonModImscpTocPopoverComponent { +export class AddonModImscpTocPage { items = []; + selected: string; constructor(navParams: NavParams, private viewCtrl: ViewController) { this.items = navParams.get('items') || []; + this.selected = navParams.get('selected'); } /** @@ -47,4 +50,11 @@ export class AddonModImscpTocPopoverComponent { getNumberForPadding(n: number): number[] { return new Array(n); } + + /** + * Close modal. + */ + closeModal(): void { + this.viewCtrl.dismiss(); + } } diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 54e78480ce7..8c35aeb95a9 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -639,6 +639,7 @@ "addon.mod_imscp.deploymenterror": "Content package error!", "addon.mod_imscp.modulenameplural": "IMS content packages", "addon.mod_imscp.showmoduledescription": "Show description", + "addon.mod_imscp.toc": "TOC", "addon.mod_lesson.answer": "Answer", "addon.mod_lesson.attempt": "Attempt: {{$a}}", "addon.mod_lesson.attemptheader": "Attempt", From 44f21222988d233ecef540a3f0777c987d6cd9c1 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 22 Aug 2019 11:35:17 +0200 Subject: [PATCH 191/241] MOBILE-3068 analytics: Don't call logEvent if user disabled --- .../providers/pushnotifications.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/core/pushnotifications/providers/pushnotifications.ts b/src/core/pushnotifications/providers/pushnotifications.ts index 17fe16bb524..0985c6b1820 100644 --- a/src/core/pushnotifications/providers/pushnotifications.ts +++ b/src/core/pushnotifications/providers/pushnotifications.ts @@ -342,11 +342,17 @@ export class CorePushNotificationsProvider { const win = window; // This feature is only present in our fork of the plugin. if (CoreConfigConstants.enableanalytics && win.PushNotification && win.PushNotification.logEvent) { - return new Promise((resolve, reject): void => { - win.PushNotification.logEvent(resolve, (error) => { - this.logger.error('Error logging firebase event', name, error); - resolve(); - }, name, data, !!filter); + + // Check if the analytics is enabled by the user. + return this.configProvider.get(CoreConstants.SETTINGS_ANALYTICS_ENABLED, true).then((enabled) => { + if (enabled) { + return new Promise((resolve, reject): void => { + win.PushNotification.logEvent(resolve, (error) => { + this.logger.error('Error logging firebase event', name, error); + resolve(); + }, name, data, !!filter); + }); + } }); } From adf30321f8000f69afec8c0a630ea7dbc141d809 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 22 Aug 2019 13:06:07 +0200 Subject: [PATCH 192/241] MOBILE-3068 login: Fix check WS enabled in 3.1 with local_mobile --- src/providers/sites.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/providers/sites.ts b/src/providers/sites.ts index 9588a3e6014..d97901828e3 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -492,6 +492,9 @@ export class CoreSitesProvider { } return data; + }, (error) => { + // Local mobile check returned an error. This only happens if the plugin is installed and it returns an error. + return rejectWithCriticalError(error); }).then((data) => { siteUrl = temporarySite.getURL(); From add7792d2442d888916783d59e89c8cf23666d8d Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 22 Aug 2019 11:22:47 +0200 Subject: [PATCH 193/241] MOBILE-3127 core: Allow defining a different timeout in wifi/3g --- src/classes/site.ts | 2 +- src/core/constants.ts | 3 ++- src/providers/sites.ts | 8 +++++--- src/providers/utils/utils.ts | 4 ++-- src/providers/ws.ts | 19 ++++++++++++++----- 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/classes/site.ts b/src/classes/site.ts index 18be199e145..4f885fe9312 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -1334,7 +1334,7 @@ export class CoreSite { return Promise.resolve({ code: 0 }); } - const promise = this.http.post(checkUrl, { service: service }).timeout(CoreConstants.WS_TIMEOUT).toPromise(); + const promise = this.http.post(checkUrl, { service: service }).timeout(this.wsProvider.getRequestTimeout()).toPromise(); return promise.then((data: any) => { if (typeof data != 'undefined' && data.errorcode === 'requirecorrectaccess') { diff --git a/src/core/constants.ts b/src/core/constants.ts index 33e20f0c25f..a6f31c3a5a0 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -39,7 +39,8 @@ export class CoreConstants { static SETTINGS_ANALYTICS_ENABLED = 'CoreSettingsAnalyticsEnabled'; // WS constants. - static WS_TIMEOUT = 30000; + static WS_TIMEOUT = 30000; // Timeout when not in WiFi. + static WS_TIMEOUT_WIFI = 30000; // Timeout when in WiFi. static WS_PREFIX = 'local_mobile_'; // Login constants. diff --git a/src/providers/sites.ts b/src/providers/sites.ts index 22df10cb79c..699843c0902 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -22,6 +22,7 @@ import { CoreSitesFactoryProvider } from './sites-factory'; import { CoreTextUtilsProvider } from './utils/text'; import { CoreUrlUtilsProvider } from './utils/url'; import { CoreUtilsProvider } from './utils/utils'; +import { CoreWSProvider } from './ws'; import { CoreConstants } from '@core/constants'; import { CoreConfigConstants } from '../configconstants'; import { CoreSite } from '@classes/site'; @@ -326,7 +327,7 @@ export class CoreSitesProvider { constructor(logger: CoreLoggerProvider, private http: HttpClient, private sitesFactory: CoreSitesFactoryProvider, private appProvider: CoreAppProvider, private translate: TranslateService, private urlUtils: CoreUrlUtilsProvider, private eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider, - private utils: CoreUtilsProvider, private injector: Injector) { + private utils: CoreUtilsProvider, private injector: Injector, private wsProvider: CoreWSProvider) { this.logger = logger.getInstance('CoreSitesProvider'); this.appDB = appProvider.getDB(); @@ -515,7 +516,8 @@ export class CoreSitesProvider { * @return {Promise} A promise to be resolved if the site exists. */ siteExists(siteUrl: string): Promise { - return this.http.post(siteUrl + '/login/token.php', {}).timeout(CoreConstants.WS_TIMEOUT).toPromise().catch(() => { + return this.http.post(siteUrl + '/login/token.php', {}).timeout(this.wsProvider.getRequestTimeout()).toPromise() + .catch(() => { // Default error messages are kinda bad, return our own message. return Promise.reject({error: this.translate.instant('core.cannotconnect')}); }).then((data: any) => { @@ -555,7 +557,7 @@ export class CoreSitesProvider { service: service }, loginUrl = siteUrl + '/login/token.php', - promise = this.http.post(loginUrl, params).timeout(CoreConstants.WS_TIMEOUT).toPromise(); + promise = this.http.post(loginUrl, params).timeout(this.wsProvider.getRequestTimeout()).toPromise(); return promise.then((data: any): any => { if (typeof data == 'undefined') { diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index 306c715259d..db08cf476fc 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -27,7 +27,6 @@ import { CoreLoggerProvider } from '../logger'; import { TranslateService } from '@ngx-translate/core'; import { CoreLangProvider } from '../lang'; import { CoreWSProvider, CoreWSError } from '../ws'; -import { CoreConstants } from '@core/constants'; /** * Deferred promise. It's similar to the result of $q.defer() in AngularJS. @@ -232,7 +231,8 @@ export class CoreUtilsProvider { initOptions.signal = controller.signal; } - return this.timeoutPromise(window.fetch(url, initOptions), CoreConstants.WS_TIMEOUT).then((response: Response) => { + return this.timeoutPromise(window.fetch(url, initOptions), this.wsProvider.getRequestTimeout()) + .then((response: Response) => { return response.redirected; }).catch((error) => { if (error.timeout && controller) { diff --git a/src/providers/ws.ts b/src/providers/ws.ts index 08d53055cd3..636b00e0bd9 100644 --- a/src/providers/ws.ts +++ b/src/providers/ws.ts @@ -253,9 +253,9 @@ export class CoreWSProvider { if (preSets.noLogin && preSets.useGet) { // Send params using GET. siteUrl += '&args=' + encodeURIComponent(ajaxData); - promise = this.http.get(siteUrl).timeout(CoreConstants.WS_TIMEOUT).toPromise(); + promise = this.http.get(siteUrl).timeout(this.getRequestTimeout()).toPromise(); } else { - promise = this.http.post(siteUrl, ajaxData).timeout(CoreConstants.WS_TIMEOUT).toPromise(); + promise = this.http.post(siteUrl, ajaxData).timeout(this.getRequestTimeout()).toPromise(); } return promise.then((data: any) => { @@ -516,6 +516,15 @@ export class CoreWSProvider { }); } + /** + * Get a request timeout based on the network connection. + * + * @return {number} Timeout in ms. + */ + getRequestTimeout(): number { + return this.appProvider.isNetworkAccessLimited() ? CoreConstants.WS_TIMEOUT : CoreConstants.WS_TIMEOUT_WIFI; + } + /** * Get the unique queue item id of the cache for a HTTP request. * @@ -542,7 +551,7 @@ export class CoreWSProvider { let promise = this.getPromiseHttp('head', url); if (!promise) { - promise = this.commonHttp.head(url).timeout(CoreConstants.WS_TIMEOUT).toPromise(); + promise = this.commonHttp.head(url).timeout(this.getRequestTimeout()).toPromise(); promise = this.setPromiseHttp(promise, 'head', url); } @@ -573,7 +582,7 @@ export class CoreWSProvider { const requestUrl = siteUrl + '&wsfunction=' + method; // Perform the post request. - const promise = this.http.post(requestUrl, ajaxData, options).timeout(CoreConstants.WS_TIMEOUT).toPromise(); + const promise = this.http.post(requestUrl, ajaxData, options).timeout(this.getRequestTimeout()).toPromise(); return promise.then((data: any) => { // Some moodle web services return null. @@ -693,7 +702,7 @@ export class CoreWSProvider { // HTTP not finished, but we should delete the promise after timeout. timeout = setTimeout(() => { delete this.ongoingCalls[queueItemId]; - }, CoreConstants.WS_TIMEOUT); + }, this.getRequestTimeout()); // HTTP finished, delete from ongoing. return promise.finally(() => { From aa9e52d0f298d2216ce34642bb1472117830da13 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 22 Aug 2019 16:40:55 +0200 Subject: [PATCH 194/241] MOBILE-3068 login: Fix no component factory found for CoreLoginSitesPage --- src/core/login/login.module.ts | 2 ++ src/core/login/pages/sites/sites.module.ts | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/core/login/login.module.ts b/src/core/login/login.module.ts index 55d40cff9b9..cb1a902ec8d 100644 --- a/src/core/login/login.module.ts +++ b/src/core/login/login.module.ts @@ -14,6 +14,7 @@ import { NgModule } from '@angular/core'; import { CoreLoginHelperProvider } from './providers/helper'; +import { CoreLoginSitesPageModule } from './pages/sites/sites.module'; // List of providers. export const CORE_LOGIN_PROVIDERS = [ @@ -24,6 +25,7 @@ export const CORE_LOGIN_PROVIDERS = [ declarations: [ ], imports: [ + CoreLoginSitesPageModule ], providers: CORE_LOGIN_PROVIDERS }) diff --git a/src/core/login/pages/sites/sites.module.ts b/src/core/login/pages/sites/sites.module.ts index 72b515f3822..7913b8d6dc0 100644 --- a/src/core/login/pages/sites/sites.module.ts +++ b/src/core/login/pages/sites/sites.module.ts @@ -27,5 +27,8 @@ import { CoreDirectivesModule } from '@directives/directives.module'; IonicPageModule.forChild(CoreLoginSitesPage), TranslateModule.forChild() ], + entryComponents: [ + CoreLoginSitesPage + ] }) export class CoreLoginSitesPageModule {} From 4f08571d39c1f980b96154c59f101c3e87c995be Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 23 Aug 2019 09:56:40 +0200 Subject: [PATCH 195/241] MOBILE-3068 ios: Fix styles in messages in iOS --- src/addon/messages/pages/discussion/discussion.scss | 9 ++++++--- .../pages/group-conversations/group-conversations.scss | 2 +- src/app/app.scss | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/addon/messages/pages/discussion/discussion.scss b/src/addon/messages/pages/discussion/discussion.scss index ca1e6623257..f17702e2435 100644 --- a/src/addon/messages/pages/discussion/discussion.scss +++ b/src/addon/messages/pages/discussion/discussion.scss @@ -5,9 +5,6 @@ $item-message-note-font-size: 75% !default; $item-message-mine-bg: $gray-light !default; ion-app.app-root page-addon-messages-discussion { - .toolbar-title { - padding: 0; - } ion-content { background-color: $gray-lighter !important; @@ -192,6 +189,8 @@ ion-app.app-root page-addon-messages-discussion { } .toolbar-title { + padding: 0; + img { @include margin-horizontal(null, 6px); } @@ -208,6 +207,10 @@ ion-app.app-root page-addon-messages-discussion { ion-icon { @include margin-horizontal(6px, null); } + + &.toolbar-title-ios { + justify-content: center; + } } } diff --git a/src/addon/messages/pages/group-conversations/group-conversations.scss b/src/addon/messages/pages/group-conversations/group-conversations.scss index 80246e1a20c..d4dd9a82162 100644 --- a/src/addon/messages/pages/group-conversations/group-conversations.scss +++ b/src/addon/messages/pages/group-conversations/group-conversations.scss @@ -40,6 +40,6 @@ ion-app.app-root .addon-message-discussion { ion-app.app-root .addon-message-discussion { h2 { - margin-top: 6px; + margin-top: 10px; } } \ No newline at end of file diff --git a/src/app/app.scss b/src/app/app.scss index 0b52e833700..1118e07714c 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -674,8 +674,8 @@ ion-app.app-root { border-radius: 50%; } - .toolbar-ios { - height: 52px; + .header .toolbar-ios { + height: $toolbar-ios-height; } // Footer with auto height. From 8651e897019c1f4fb9cecf3d27efbb503776898b Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 23 Aug 2019 10:39:34 +0200 Subject: [PATCH 196/241] MOBILE-2670 core: Fix filename of uploaded files in desktop --- src/core/emulator/providers/file-transfer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/emulator/providers/file-transfer.ts b/src/core/emulator/providers/file-transfer.ts index cfef1151209..f8ab362c90b 100644 --- a/src/core/emulator/providers/file-transfer.ts +++ b/src/core/emulator/providers/file-transfer.ts @@ -380,7 +380,7 @@ export class FileTransferObjectMock extends FileTransferObject { for (const name in params) { fd.append(name, params[name]); } - fd.append('file', file); + fd.append('file', file, fileName); xhr.send(fd); }).catch(reject); From 15ecd70e18ee6ea8fb581cf08a6cf536581c3e78 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 23 Aug 2019 12:34:01 +0200 Subject: [PATCH 197/241] MOBILE-3068 notifications: Fix view page opened in phantom tab --- src/addon/notifications/components/actions/actions.ts | 3 ++- .../components/actions/addon-notifications-actions.html | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/addon/notifications/components/actions/actions.ts b/src/addon/notifications/components/actions/actions.ts index 2d56bc7688a..c2a6f23bb0d 100644 --- a/src/addon/notifications/components/actions/actions.ts +++ b/src/addon/notifications/components/actions/actions.ts @@ -31,7 +31,8 @@ export class AddonNotificationsActionsComponent implements OnInit { actions: CoreContentLinksAction[] = []; - constructor(private contentLinksDelegate: CoreContentLinksDelegate, private sitesProvider: CoreSitesProvider) {} + constructor(private contentLinksDelegate: CoreContentLinksDelegate, private sitesProvider: CoreSitesProvider, + public navCtrl: NavController) {} /** * Component being initialized. diff --git a/src/addon/notifications/components/actions/addon-notifications-actions.html b/src/addon/notifications/components/actions/addon-notifications-actions.html index 14425705d19..1f386603bf5 100644 --- a/src/addon/notifications/components/actions/addon-notifications-actions.html +++ b/src/addon/notifications/components/actions/addon-notifications-actions.html @@ -1,6 +1,6 @@ - From 58591bdaed0b889adc9cfd68bb578cab6aa638b5 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 23 Aug 2019 13:28:40 +0200 Subject: [PATCH 198/241] MOBILE-3068 notes: Fix pencil button not disappearing --- src/addon/notes/components/list/addon-notes-list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addon/notes/components/list/addon-notes-list.html b/src/addon/notes/components/list/addon-notes-list.html index 7908279f811..a959b746f43 100644 --- a/src/addon/notes/components/list/addon-notes-list.html +++ b/src/addon/notes/components/list/addon-notes-list.html @@ -1,5 +1,5 @@ - From b24cbab4004d8c070327236371951f47371ffd7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 22 Aug 2019 12:49:38 +0200 Subject: [PATCH 199/241] MOBILE-3021 calendar: UX improvements --- scripts/langindex.json | 1 + .../calendar/addon-calendar-calendar.html | 17 +++---- .../components/calendar/calendar.scss | 44 ++++++++++++++----- .../calendar/components/calendar/calendar.ts | 38 +++++++++++++++- src/addon/calendar/lang/en.json | 1 + src/addon/calendar/pages/day/day.html | 11 +++-- src/addon/calendar/pages/day/day.ts | 20 ++++++++- src/addon/calendar/pages/index/index.html | 11 ++--- src/addon/calendar/pages/index/index.scss | 3 -- src/addon/calendar/pages/list/list.html | 3 +- src/assets/lang/en.json | 1 + 11 files changed, 109 insertions(+), 41 deletions(-) delete mode 100644 src/addon/calendar/pages/index/index.scss diff --git a/scripts/langindex.json b/scripts/langindex.json index 8819689b286..fe54142bdcf 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -91,6 +91,7 @@ "addon.calendar.calendarreminders": "local_moodlemobileapp", "addon.calendar.confirmeventdelete": "calendar", "addon.calendar.confirmeventseriesdelete": "calendar", + "addon.calendar.currentmonth": "local_moodlemobileapp", "addon.calendar.daynext": "calendar", "addon.calendar.dayprev": "calendar", "addon.calendar.defaultnotificationtime": "local_moodlemobileapp", diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index bf5c2942256..03037725123 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -1,9 +1,9 @@ - + + + @@ -31,15 +31,16 @@

{{ periodName }}

- {{ day.shortname | translate }} + {{ day.shortname | translate }} + {{ day.fullname | translate }} - -

{{ day.mday }}

+ +

{{ day.mday }}

@@ -47,12 +48,12 @@

{{ periodName }}

-

+

- {{ event.timestart * 1000 | coreFormatDate: timeFormat }} + {{event.name}}

diff --git a/src/addon/calendar/components/calendar/calendar.scss b/src/addon/calendar/components/calendar/calendar.scss index 278e56bcf12..ff2d77c97aa 100644 --- a/src/addon/calendar/components/calendar/calendar.scss +++ b/src/addon/calendar/components/calendar/calendar.scss @@ -32,17 +32,34 @@ ion-app.app-root addon-calendar-calendar { @include border-end(0, null, null); @include padding(null, 8px, null, null); } + + &.addon-calendar-event-past-day > .addon-calendar-dot-types, + &.addon-calendar-event-past-day > .addon-calendar-day-events { + opacity: 0.5; + } + .addon-calendar-day-number { - height: 24px; - line-height: 24px; - width: max-content; - min-width: 24px; - text-align: center; - font-weight: 500; - display: inline-block; - margin: 3px; - } - &.today .addon-calendar-day-number { + margin: 0; + + span { + line-height: 24px; + font-weight: 500; + display: inline-block; + margin: 3px; + width: max-content; + width: 24px; + height: 24px; + text-align: center; + } + } + + @include media-breakpoint-up(md) { + .addon-calendar-day-number { + text-align: left; + } + } + + &.today .addon-calendar-day-number span { background-color: $calendar-today-bgcolor; color: $calendar-today-color; @@ -58,6 +75,10 @@ ion-app.app-root addon-calendar-calendar { overflow: hidden; white-space: nowrap; + &.addon-calendar-event-past { + opacity: 0.5; + } + .addon-calendar-event-name { font-weight: 500; } @@ -81,7 +102,6 @@ ion-app.app-root addon-calendar-calendar { } .addon-calendar-weekday { - color: $gray-dark; border-bottom: 1px solid $list-md-border-color; } @@ -130,6 +150,6 @@ ion-app.app-root addon-calendar-calendar { width: 16px; height: 16px; display: inline-block; - vertical-align: middle; + vertical-align: bottom; } } \ No newline at end of file diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index a620fe6eee9..985f284c8e2 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -48,6 +48,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest loaded = false; timeFormat: string; isCurrentMonth: boolean; + isPastMonth: boolean; protected year: number; protected month: number; @@ -57,6 +58,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest protected offlineEvents: {[monthId: string]: {[day: number]: any[]}} = {}; // Offline events classified in month & day. protected offlineEditedEventsIds = []; // IDs of events edited in offline. protected deletedEvents = []; // Events deleted in offline. + protected currentTime: number; // Observers. protected undeleteEventObserver: any; @@ -200,6 +202,28 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.weekDays = this.calendarProvider.getWeekDays(result.daynames[0].dayno); this.weeks = result.weeks; + this.calculateIsCurrentMonth(); + + if (this.isCurrentMonth) { + let isPast = true; + this.weeks.forEach((week) => { + week.days.some((day) => { + day.ispast = isPast && !day.istoday; + isPast = day.ispast; + + if (day.istoday) { + day.events.forEach((event) => { + event.ispast = this.isEventPast(event); + }); + + return true; + } + + return day.istoday; + }); + }); + } + // Merge the online events with offline data. this.mergeEvents(); @@ -288,7 +312,6 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); this.decreaseMonth(); }).finally(() => { - this.calculateIsCurrentMonth(); this.loaded = true; }); } @@ -305,7 +328,6 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); this.increaseMonth(); }).finally(() => { - this.calculateIsCurrentMonth(); this.loaded = true; }); } @@ -336,7 +358,10 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest calculateIsCurrentMonth(): void { const now = new Date(); + this.currentTime = this.timeUtils.timestamp(); + this.isCurrentMonth = this.year == now.getFullYear() && this.month == now.getMonth() + 1; + this.isPastMonth = this.year < now.getFullYear() || (this.year == now.getFullYear() && this.month < now.getMonth() + 1); } /** @@ -466,6 +491,15 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest }); } + /** + * Returns if the event is in the past or not. + * @param {any} event Event object. + * @return {boolean} True if it's in the past. + */ + isEventPast(event: any): boolean { + return (event.timestart + event.timeduration) < this.currentTime; + } + /** * Component destroyed. */ diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index be2bab15ac5..ea79d56e51c 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -6,6 +6,7 @@ "calendarreminders": "Calendar reminders", "confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?", "confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?", + "currentmonth": "Current Month", "daynext": "Next day", "dayprev": "Previous day", "defaultnotificationtime": "Default notification time", diff --git a/src/addon/calendar/pages/day/day.html b/src/addon/calendar/pages/day/day.html index cec0955aca7..9cfd3705a0a 100644 --- a/src/addon/calendar/pages/day/day.html +++ b/src/addon/calendar/pages/day/day.html @@ -2,13 +2,12 @@ {{ 'addon.calendar.calendarevents' | translate }} - + @@ -49,7 +48,7 @@

{{ periodName }}

- +

@@ -62,7 +61,7 @@

{{ 'core.deletedoffline' | translate }} -
+
diff --git a/src/addon/calendar/pages/day/day.ts b/src/addon/calendar/pages/day/day.ts index 73202cfd35a..cc930e7285f 100644 --- a/src/addon/calendar/pages/day/day.ts +++ b/src/addon/calendar/pages/day/day.ts @@ -51,6 +51,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { protected deletedEvents = []; // Events deleted in offline. protected timeFormat: string; protected currentMoment: moment.Moment; + protected currentTime: number; // Observers. protected newEventObserver: any; @@ -74,6 +75,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { isOnline = false; syncIcon: string; isCurrentDay: boolean; + isPastDay: boolean; constructor(localNotificationsProvider: CoreLocalNotificationsProvider, navParams: NavParams, @@ -308,9 +310,12 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { // Filter events by course. this.filterEvents(); + this.calculateIsCurrentDay(); + // Re-calculate the formatted time so it uses the device date. const dayTime = this.currentMoment.unix() * 1000; this.events.forEach((event) => { + event.ispast = this.isPastDay || (this.isCurrentDay && this.isEventPast(event)); promises.push(this.calendarProvider.formatEventTime(event, this.timeFormat, true, dayTime).then((time) => { event.formattedtime = time; })); @@ -555,7 +560,11 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { calculateIsCurrentDay(): void { const now = new Date(); + this.currentTime = this.timeUtils.timestamp(); + this.isCurrentDay = this.year == now.getFullYear() && this.month == now.getMonth() + 1 && this.day == now.getDate(); + this.isPastDay = this.year < now.getFullYear() || (this.year == now.getFullYear() && this.month < now.getMonth()) || + (this.year == now.getFullYear() && this.month == now.getMonth() + 1 && this.day < now.getDate()); } /** @@ -600,7 +609,6 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); this.decreaseDay(); }).finally(() => { - this.calculateIsCurrentDay(); this.loaded = true; }); } @@ -617,7 +625,6 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); this.increaseDay(); }).finally(() => { - this.calculateIsCurrentDay(); this.loaded = true; }); } @@ -665,6 +672,15 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { return false; } + /** + * Returns if the event is in the past or not. + * @param {any} event Event object. + * @return {boolean} True if it's in the past. + */ + isEventPast(event: any): boolean { + return (event.timestart + event.timeduration) < this.currentTime; + } + /** * Page destroyed. */ diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html index fc6386e368a..d2d6d38b089 100644 --- a/src/addon/calendar/pages/index/index.html +++ b/src/addon/calendar/pages/index/index.html @@ -2,16 +2,13 @@ {{ (showCalendar ? 'addon.calendar.calendarevents' : 'addon.calendar.upcomingevents') | translate }} - - + + diff --git a/src/addon/calendar/pages/index/index.scss b/src/addon/calendar/pages/index/index.scss deleted file mode 100644 index 213b8abe5bc..00000000000 --- a/src/addon/calendar/pages/index/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -page-addon-calendar-index .toolbar-ios ion-title { - @include padding-horizontal(null, 120px); -} \ No newline at end of file diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index bd5c848adab..2d85b3f1d5a 100644 --- a/src/addon/calendar/pages/list/list.html +++ b/src/addon/calendar/pages/list/list.html @@ -3,7 +3,8 @@ {{ 'addon.calendar.calendarevents' | translate }} diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 8c35aeb95a9..831223df192 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -90,6 +90,7 @@ "addon.calendar.calendarreminders": "Calendar reminders", "addon.calendar.confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?", "addon.calendar.confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?", + "addon.calendar.currentmonth": "Current Month", "addon.calendar.daynext": "Next day", "addon.calendar.dayprev": "Previous day", "addon.calendar.defaultnotificationtime": "Default notification time", From cb9b936b3ccb25d96926b488ea43992f6a44a31c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 22 Aug 2019 15:38:13 +0200 Subject: [PATCH 200/241] MOBILE-3068 styles: Fix RTE image sizing --- src/theme/format-text.scss | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/theme/format-text.scss b/src/theme/format-text.scss index a14ee433e86..8365e736343 100644 --- a/src/theme/format-text.scss +++ b/src/theme/format-text.scss @@ -96,8 +96,6 @@ ion-app.app-root core-rich-text-editor .core-rte-editor { .atto_image_button_right { vertical-align: middle; max-width: 100%; - height: auto; - width: auto; display: inline-block; margin: 0 0.5em; @@ -105,8 +103,6 @@ ion-app.app-root core-rich-text-editor .core-rte-editor { /* If the image is display: block then linking the image to URLs won't work. */ /*display: inline-block;*/ max-width: 100%; - height: auto; - width: auto; } } @@ -179,6 +175,24 @@ ion-app.app-root core-rich-text-editor .core-rte-editor { } } +// Those styles are omitted on RTE. +ion-app.app-root core-format-text, +ion-app.app-root .item core-format-text { + .atto_image_button_text-top, + .atto_image_button_middle, + .atto_image_button_text-bottom, + .atto_image_button_left, + .atto_image_button_right { + height: auto; + width: auto; + + &.img-responsive { + height: auto; + width: auto; + } + } +} + // Special fixes // ------------------------- ion-app.app-root { From 51403efcca33f4827511e5d3da5f84eb3d2b2eb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 22 Aug 2019 17:15:33 +0200 Subject: [PATCH 201/241] MOBILE-3068 core: Fix some uncaught promises --- src/core/course/course.module.ts | 4 +++- src/core/siteplugins/providers/helper.ts | 2 ++ src/providers/sites.ts | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/core/course/course.module.ts b/src/core/course/course.module.ts index d14844582ab..52a686a40b8 100644 --- a/src/core/course/course.module.ts +++ b/src/core/course/course.module.ts @@ -96,7 +96,9 @@ export class CoreCourseModule { eventsProvider.on(CoreEventsProvider.LOGIN, () => { // Log the app is open to keep user in online status. setTimeout(() => { - cronDelegate.forceCronHandlerExecution(logHandler.name); + cronDelegate.forceCronHandlerExecution(logHandler.name).catch((e) => { + // Ignore errors here, since probably login is not complete: it happens on token invalid. + }); }, 1000); }); } diff --git a/src/core/siteplugins/providers/helper.ts b/src/core/siteplugins/providers/helper.ts index f046cdbb444..b6c33d0fcfe 100644 --- a/src/core/siteplugins/providers/helper.ts +++ b/src/core/siteplugins/providers/helper.ts @@ -114,6 +114,8 @@ export class CoreSitePluginsHelperProvider { eventsProvider.trigger(CoreEventsProvider.SITE_PLUGINS_LOADED, {}, data.siteId); }); } + }).catch((e) => { + // Ignore errors here. }).finally(() => { this.sitePluginsProvider.setPluginsFetched(); }); diff --git a/src/providers/sites.ts b/src/providers/sites.ts index 5bcb8e9388a..515d7117aac 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -1385,6 +1385,8 @@ export class CoreSitesProvider { this.eventsProvider.trigger(CoreEventsProvider.SITE_UPDATED, info, siteId); }); }); + }).catch((error) => { + // Ignore that we cannot fetch site info. Probably the auth token is invalid. }); }); } From 6269849dff031eb1038c2ab09c045a32747c2da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 23 Aug 2019 09:07:36 +0200 Subject: [PATCH 202/241] MOBILE-3025 blocks: Rollback scroll follow implementation --- .../core-block-course-blocks.html | 18 ++-- .../course-blocks/course-blocks.scss | 30 ------- .../components/course-blocks/course-blocks.ts | 89 +------------------ 3 files changed, 11 insertions(+), 126 deletions(-) diff --git a/src/core/block/components/course-blocks/core-block-course-blocks.html b/src/core/block/components/course-blocks/core-block-course-blocks.html index ee6af5a26a8..a34ada03c63 100644 --- a/src/core/block/components/course-blocks/core-block-course-blocks.html +++ b/src/core/block/components/course-blocks/core-block-course-blocks.html @@ -3,14 +3,12 @@
-
- - - - - - - - -
+ + + + + + + +
diff --git a/src/core/block/components/course-blocks/course-blocks.scss b/src/core/block/components/course-blocks/course-blocks.scss index cc2c9f3c08e..61fcad7beed 100644 --- a/src/core/block/components/course-blocks/course-blocks.scss +++ b/src/core/block/components/course-blocks/course-blocks.scss @@ -13,10 +13,6 @@ ion-app.app-root core-block-course-blocks { &.core-has-blocks { @include media-breakpoint-up(md) { - @include position(0, 0, 0, 0); - - position: absolute; - display: flex; flex-direction: row; @@ -29,46 +25,20 @@ ion-app.app-root core-block-course-blocks { } div.core-course-blocks-side { - position: relative; - @include position(auto, 0, auto, auto); max-width: $core-side-blocks-max-width; min-width: $core-side-blocks-min-width; @include border-start(1px, solid, $list-md-border-color); - - .core-course-blocks-side-scroll { - position: absolute; - top: 0; - max-width: 100%; - min-width: 100%; - - &.core-course-blocks-fixed-bottom { - position: fixed; - bottom: 0; - top: auto; - transform: none !important; - } - - core-block { - max-width: $core-side-blocks-max-width; - min-width: $core-side-blocks-min-width; - } - } } } @include media-breakpoint-down(sm) { // Disable scroll on individual columns. div.core-course-blocks-side { - transform: none !important; height: auto; &.core-hide-blocks { display: none; } - - .core-course-blocks-side-scroll { - transform: none !important; - } } } } diff --git a/src/core/block/components/course-blocks/course-blocks.ts b/src/core/block/components/course-blocks/course-blocks.ts index 7db1ffa8cb8..c1b849c560e 100644 --- a/src/core/block/components/course-blocks/course-blocks.ts +++ b/src/core/block/components/course-blocks/course-blocks.ts @@ -12,10 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, ViewChildren, Input, OnInit, QueryList, ElementRef, OnDestroy } from '@angular/core'; +import { Component, ViewChildren, Input, OnInit, QueryList, ElementRef } from '@angular/core'; import { Content } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreAppProvider } from '@providers/app'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreBlockComponent } from '../block/block'; import { CoreBlockHelperProvider } from '../../providers/helper'; @@ -27,7 +26,7 @@ import { CoreBlockHelperProvider } from '../../providers/helper'; selector: 'core-block-course-blocks', templateUrl: 'core-block-course-blocks.html', }) -export class CoreBlockCourseBlocksComponent implements OnInit, OnDestroy { +export class CoreBlockCourseBlocksComponent implements OnInit { @Input() courseId: number; @Input() hideBlocks = false; @@ -39,16 +38,10 @@ export class CoreBlockCourseBlocksComponent implements OnInit, OnDestroy { blocks = []; protected element: HTMLElement; - protected lastScroll; - protected translationY = 0; - protected blocksScrollHeight = 0; - protected sideScroll: HTMLElement; - protected vpHeight = 0; // Viewport height. - protected scrollWorking = false; constructor(private domUtils: CoreDomUtilsProvider, private courseProvider: CoreCourseProvider, protected blockHelper: CoreBlockHelperProvider, element: ElementRef, - protected content: Content, protected appProvider: CoreAppProvider) { + protected content: Content) { this.element = element.nativeElement; } @@ -58,83 +51,9 @@ export class CoreBlockCourseBlocksComponent implements OnInit, OnDestroy { ngOnInit(): void { this.loadContent().finally(() => { this.dataLoaded = true; - - window.addEventListener('resize', this.initScroll.bind(this)); - }); - } - - /** - * Setup scrolling. - */ - protected initScroll(): void { - if (this.blocks.length <= 0) { - return; - } - - const scroll: HTMLElement = this.content && this.content.getScrollElement(); - - this.domUtils.waitElementToExist(() => scroll && scroll.querySelector('.core-course-blocks-side')).then((sideElement) => { - const contentHeight = parseInt(this.content.getNativeElement().querySelector('.scroll-content').scrollHeight, 10); - - this.sideScroll = scroll.querySelector('.core-course-blocks-side-scroll'); - this.blocksScrollHeight = this.sideScroll.scrollHeight; - this.vpHeight = sideElement.clientHeight; - - // Check if needed and event was not init before. - if (this.appProvider.isWide() && this.vpHeight && contentHeight > this.vpHeight && - this.blocksScrollHeight > this.vpHeight) { - if (typeof this.lastScroll == 'undefined') { - this.lastScroll = 0; - scroll.addEventListener('scroll', this.scrollFunction.bind(this)); - } - this.scrollWorking = true; - } else { - this.sideScroll.style.transform = 'translate(0, 0)'; - this.sideScroll.classList.remove('core-course-blocks-fixed-bottom'); - this.scrollWorking = false; - } - }).catch(() => { - // Ignore errors. }); } - /** - * Scroll function that moves the sidebar if needed. - * - * @param {Event} e Event to get the target from. - */ - protected scrollFunction(e: Event): void { - if (!this.scrollWorking) { - return; - } - - const target: any = e.target, - top = parseInt(target.scrollTop, 10), - goingUp = top < this.lastScroll; - if (goingUp) { - this.sideScroll.classList.remove('core-course-blocks-fixed-bottom'); - if (top <= this.translationY ) { - // Fixed to top. - this.translationY = top; - this.sideScroll.style.transform = 'translate(0, ' + this.translationY + 'px)'; - } - } else if (top - this.translationY >= (this.blocksScrollHeight - this.vpHeight)) { - // Fixed to bottom. - this.sideScroll.classList.add('core-course-blocks-fixed-bottom'); - this.translationY = top - (this.blocksScrollHeight - this.vpHeight); - this.sideScroll.style.transform = 'translate(0, ' + this.translationY + 'px)'; - } - - this.lastScroll = top; - } - - /** - * Component destroyed. - */ - ngOnDestroy(): void { - window.removeEventListener('resize', this.initScroll); - } - /** * Invalidate blocks data. * @@ -175,8 +94,6 @@ export class CoreBlockCourseBlocksComponent implements OnInit, OnDestroy { this.element.classList.remove('core-no-blocks'); this.content.getElementRef().nativeElement.classList.add('core-course-block-with-blocks'); - - this.initScroll(); } else { this.element.classList.remove('core-has-blocks'); this.element.classList.add('core-no-blocks'); From f3a51a3717f148e70a53c43024f3ca59bbb08467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 23 Aug 2019 10:14:28 +0200 Subject: [PATCH 203/241] MOBILE-3068 tabs: Fix scroll problem on calculating tabs --- src/components/tabs/tabs.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index 921143228bf..bf1b3c6cdb9 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -234,7 +234,15 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe */ calculateTabBarHeight(): void { this.tabBarHeight = this.topTabsElement.offsetHeight; - this.originalTabsContainer.style.paddingBottom = this.tabBarHeight + 'px'; + + if (this.tabsShown) { + // Smooth translation. + this.topTabsElement.style.transform = 'translateY(-' + this.lastScroll + 'px)'; + this.originalTabsContainer.style.transform = 'translateY(-' + this.lastScroll + 'px)'; + this.originalTabsContainer.style.paddingBottom = this.tabBarHeight - this.lastScroll + 'px'; + } else { + this.tabBarElement.classList.add('tabs-hidden'); + } } /** From fd6c700ff8587604641473426596f40ab3382622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 23 Aug 2019 13:33:53 +0200 Subject: [PATCH 204/241] MOBILE-3068 blogs: Fix infinite loading performance on my entries --- .../entries/addon-blog-entries.html | 4 +- src/addon/blog/components/entries/entries.ts | 71 ++++++++++++++++--- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/src/addon/blog/components/entries/addon-blog-entries.html b/src/addon/blog/components/entries/addon-blog-entries.html index 690930091c4..b3a41ae72b6 100644 --- a/src/addon/blog/components/entries/addon-blog-entries.html +++ b/src/addon/blog/components/entries/addon-blog-entries.html @@ -4,9 +4,9 @@
- + {{ 'addon.blog.showonlyyourentries' | translate }} - + >
diff --git a/src/addon/blog/components/entries/entries.ts b/src/addon/blog/components/entries/entries.ts index 071e53fe618..89a5b091770 100644 --- a/src/addon/blog/components/entries/entries.ts +++ b/src/addon/blog/components/entries/entries.ts @@ -15,6 +15,7 @@ import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { Content } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSitesProvider } from '@providers/sites'; import { CoreUserProvider } from '@core/user/providers/user'; import { AddonBlogProvider } from '../../providers/blog'; @@ -38,6 +39,9 @@ export class AddonBlogEntriesComponent implements OnInit { protected filter = {}; protected pageLoaded = 0; + protected userPageLoaded = 0; + protected canLoadMoreEntries = false; + protected canLoadMoreUserEntries = true; @ViewChild(Content) content: Content; @@ -46,14 +50,14 @@ export class AddonBlogEntriesComponent implements OnInit { loadMoreError = false; entries = []; currentUserId: number; - showMyIssuesToggle = false; + showMyEntriesToggle = false; onlyMyEntries = false; component = AddonBlogProvider.COMPONENT; commentsEnabled: boolean; tagsEnabled: boolean; constructor(protected blogProvider: AddonBlogProvider, protected domUtils: CoreDomUtilsProvider, - protected userProvider: CoreUserProvider, sitesProvider: CoreSitesProvider, + protected userProvider: CoreUserProvider, sitesProvider: CoreSitesProvider, protected utils: CoreUtilsProvider, protected commentsProvider: CoreCommentsProvider, private tagProvider: CoreTagProvider) { this.currentUserId = sitesProvider.getCurrentSiteUserId(); } @@ -65,6 +69,7 @@ export class AddonBlogEntriesComponent implements OnInit { if (this.userId) { this.filter['userid'] = this.userId; } + this.showMyEntriesToggle = !this.userId; if (this.courseId) { this.filter['courseid'] = this.courseId; @@ -107,9 +112,12 @@ export class AddonBlogEntriesComponent implements OnInit { if (refresh) { this.pageLoaded = 0; + this.userPageLoaded = 0; } - return this.blogProvider.getEntries(this.filter, this.pageLoaded).then((result) => { + const loadPage = this.onlyMyEntries ? this.userPageLoaded : this.pageLoaded; + + return this.blogProvider.getEntries(this.filter, loadPage).then((result) => { const promises = result.entries.map((entry) => { switch (entry.publishstate) { case 'draft': @@ -134,16 +142,25 @@ export class AddonBlogEntriesComponent implements OnInit { }); if (refresh) { - this.showMyIssuesToggle = false; this.entries = result.entries; } else { - this.entries = this.entries.concat(result.entries); + this.entries = this.utils.uniqueArray(this.entries.concat(result.entries), 'id').sort((a, b) => { + return b.created - a.created; + }); } - this.canLoadMore = result.totalentries > this.entries.length; - this.pageLoaded++; - - this.showMyIssuesToggle = !this.userId; + if (this.onlyMyEntries) { + const count = this.entries.filter((entry) => { + return entry.userid == this.currentUserId; + }).length; + this.canLoadMoreUserEntries = result.totalentries > count; + this.canLoadMore = this.canLoadMoreUserEntries; + this.userPageLoaded++; + } else { + this.canLoadMoreEntries = result.totalentries > this.entries.length; + this.canLoadMore = this.canLoadMoreEntries; + this.pageLoaded++; + } return Promise.all(promises); }).catch((message) => { @@ -154,6 +171,30 @@ export class AddonBlogEntriesComponent implements OnInit { }); } + /** + * Toggle between showing only my entries or not. + * + * @param {boolean} enabled If true, filter my entries. False otherwise. + */ + onlyMyEntriesToggleChanged(enabled: boolean): void { + if (enabled) { + const count = this.entries.filter((entry) => { + return entry.userid == this.currentUserId; + }).length; + this.userPageLoaded = Math.floor(count / AddonBlogProvider.ENTRIES_PER_PAGE); + this.filter['userid'] = this.currentUserId; + + if (count == 0 && this.canLoadMoreUserEntries) { + // First time but no entry loaded. Try to load some. + this.loadMore(); + } + } else { + delete this.filter['userid']; + } + + this.canLoadMore = enabled ? this.canLoadMoreUserEntries : this.canLoadMoreEntries; + } + /** * Function to load more entries. * @@ -178,7 +219,17 @@ export class AddonBlogEntriesComponent implements OnInit { promises.push(this.blogProvider.invalidateEntries(this.filter)); - Promise.all(promises).finally(() => { + if (this.showMyEntriesToggle) { + this.filter['userid'] = this.currentUserId; + promises.push(this.blogProvider.invalidateEntries(this.filter)); + + if (!this.showMyEntriesToggle) { + delete this.filter['userid']; + } + + } + + this.utils.allPromises(promises).finally(() => { this.fetchEntries(true).finally(() => { if (refresher) { refresher.complete(); From 89a385b08b0554609ecfc7fb8386de4ab8096b61 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 23 Aug 2019 14:07:46 +0200 Subject: [PATCH 205/241] MOBILE-3068 notes: Fix error displayed when creating offline --- src/addon/notes/components/list/list.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/addon/notes/components/list/list.ts b/src/addon/notes/components/list/list.ts index e9b6ffd7a17..cf6f54afa39 100644 --- a/src/addon/notes/components/list/list.ts +++ b/src/addon/notes/components/list/list.ts @@ -186,7 +186,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { this.notesLoaded = false; } - this.refreshNotes(true); + this.refreshNotes(false); } else if (data && data.type && data.type != this.type) { this.type = data.type; this.typeChanged(); @@ -209,7 +209,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { this.notesProvider.deleteNote(note, this.courseId).then(() => { this.showDelete = false; - this.refreshNotes(true); + this.refreshNotes(false); this.domUtils.showToast('addon.notes.eventnotedeleted', true, 3000); }).catch((error) => { From 789eb299b045e600aa4b86379d466c221700288d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 23 Aug 2019 15:11:36 +0200 Subject: [PATCH 206/241] MOBILE-3068 page: Fix base64 images shown --- src/directives/external-content.ts | 11 +++++++++++ src/directives/format-text.ts | 11 +++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/directives/external-content.ts b/src/directives/external-content.ts index 68937d89956..4043976aa2f 100644 --- a/src/directives/external-content.ts +++ b/src/directives/external-content.ts @@ -49,6 +49,8 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges { protected logger; protected initialized = false; + invalid = false; + constructor(element: ElementRef, logger: CoreLoggerProvider, private filepoolProvider: CoreFilepoolProvider, private platform: Platform, private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider) { @@ -141,6 +143,15 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges { } } else { + this.invalid = true; + + return; + } + + // Avoid handling data url's. + if (url.indexOf('data:') === 0) { + this.invalid = true; + return; } diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index ea7b02b9a02..c04732203ef 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -409,7 +409,12 @@ export class CoreFormatTextDirective implements OnChanges { // Walk through the content to find images, and add our directive. images.forEach((img: HTMLElement) => { this.addMediaAdaptClass(img); - externalImages.push(this.addExternalContent(img)); + + const externalImage = this.addExternalContent(img); + if (!externalImage.invalid) { + externalImages.push(externalImage); + } + if (this.utils.isTrueOrOne(this.adaptImg) && !img.classList.contains('icon')) { this.adaptImage(img); } @@ -475,7 +480,9 @@ export class CoreFormatTextDirective implements OnChanges { promise = Promise.resolve(); } - return promise.then(() => { + return promise.catch(() => { + // Ignore errors. So content gets always shown. + }).then(() => { return div; }); }); From 4053d5bea4e1dd936a9a8bfd41cae3efd7af2d2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 23 Aug 2019 15:29:32 +0200 Subject: [PATCH 207/241] MOBILE-3068 lang: Remove warning when adding new strings --- scripts/lang_functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/lang_functions.php b/scripts/lang_functions.php index 56158a469d2..af1e2ab3b31 100644 --- a/scripts/lang_functions.php +++ b/scripts/lang_functions.php @@ -316,7 +316,7 @@ function save_key($key, $value, $path) { $file = file_get_contents($filePath); $file = (array) json_decode($file); $value = html_entity_decode($value); - if ($file[$key] != $value) { + if (!isset($file[$key]) || $file[$key] != $value) { $file[$key] = $value; ksort($file); file_put_contents($filePath, str_replace('\/', '/', json_encode($file, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT))); From f8240f5a459ccc8432f617281c3d3e5743ec2b6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 23 Aug 2019 16:07:25 +0200 Subject: [PATCH 208/241] MOBILE-3068 ios: Style action sheets --- src/app/app.md.scss | 9 +++++++++ src/app/app.scss | 9 --------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/app.md.scss b/src/app/app.md.scss index bee37227555..a97ce6e952b 100644 --- a/src/app/app.md.scss +++ b/src/app/app.md.scss @@ -70,6 +70,15 @@ ion-app.app-root.md { padding-top: 0; margin-top: $action-sheet-md-title-padding-top; } + + @media (min-height: 500px) { + .action-sheet-wrapper { + bottom: 0; + top: initial; + max-height: 50%; + height: 100%; + } + } } } diff --git a/src/app/app.scss b/src/app/app.scss index 1118e07714c..9fbf324f0ec 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -609,15 +609,6 @@ ion-app.app-root { } } - @media (min-height: 500px) { - .action-sheet-wrapper { - bottom: 0; - top: initial; - max-height: 50%; - height: 100%; - } - } - .alert-message { overflow-y: auto; } From 6dd6154633ef1be46060c3646f419e3053cef421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 23 Aug 2019 16:08:12 +0200 Subject: [PATCH 209/241] MOBILE-3025 blocks: Add scroll to online users --- src/addon/block/onlineusers/onlineusers.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/addon/block/onlineusers/onlineusers.scss b/src/addon/block/onlineusers/onlineusers.scss index eb05cef2edf..009a347b807 100644 --- a/src/addon/block/onlineusers/onlineusers.scss +++ b/src/addon/block/onlineusers/onlineusers.scss @@ -1,4 +1,12 @@ .addon-block-online-users core-block-pre-rendered .core-block-content { + max-height: 200px; + overflow-y: auto; + .item-inner, + .input-wrapper { + overflow-y: visible; + align-self: start; + } + .list { @include margin-horizontal(0); -webkit-padding-start: 0; From ae3d488877f8887113c76c051827b414892b0e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 23 Aug 2019 16:08:37 +0200 Subject: [PATCH 210/241] MOBILE-3025 blocks: Show empty box centered --- .../block/components/course-blocks/course-blocks.scss | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/core/block/components/course-blocks/course-blocks.scss b/src/core/block/components/course-blocks/course-blocks.scss index 61fcad7beed..ef2da86c0a3 100644 --- a/src/core/block/components/course-blocks/course-blocks.scss +++ b/src/core/block/components/course-blocks/course-blocks.scss @@ -29,6 +29,17 @@ ion-app.app-root core-block-course-blocks { min-width: $core-side-blocks-min-width; @include border-start(1px, solid, $list-md-border-color); } + + .core-course-blocks-content, + div.core-course-blocks-side { + position: relative; + height: 100%; + + .core-loading-center, + core-loading.core-loading-loaded { + position: initial; + } + } } @include media-breakpoint-down(sm) { From 087e7ac2467fd4968ca3163d46a1576df965fa26 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 23 Aug 2019 16:17:31 +0200 Subject: [PATCH 211/241] MOBILE-3068 resource: Fix embedded image not displayed --- src/directives/external-content.ts | 29 ++++++++++++++++++++++------- src/directives/format-text.ts | 5 +++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/directives/external-content.ts b/src/directives/external-content.ts index 68937d89956..94ebfcbcb09 100644 --- a/src/directives/external-content.ts +++ b/src/directives/external-content.ts @@ -45,6 +45,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges { @Input() poster?: string; @Output() onLoad = new EventEmitter(); // Emitted when content is loaded. Only for images. + loaded = false; protected element: HTMLElement; protected logger; protected initialized = false; @@ -192,6 +193,10 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges { this.addSource(url); } + if (tagName === 'IMG') { + this.waitForLoad(); + } + return Promise.reject(null); } @@ -227,13 +232,8 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges { this.addSource(finalUrl); } else { if (tagName === 'IMG') { - const listener = (): void => { - this.element.removeEventListener('load', listener); - this.element.removeEventListener('error', listener); - this.onLoad.emit(); - }; - this.element.addEventListener('load', listener); - this.element.addEventListener('error', listener); + this.loaded = false; + this.waitForLoad(); } this.element.setAttribute(targetAttr, finalUrl); this.element.setAttribute('data-original-' + targetAttr, url); @@ -299,4 +299,19 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges { this.element.setAttribute('style', inlineStyles); }); } + + /** + * Wait for the image to be loaded or error, and emit an event when it happens. + */ + protected waitForLoad(): void { + const listener = (): void => { + this.element.removeEventListener('load', listener); + this.element.removeEventListener('error', listener); + this.onLoad.emit(); + this.loaded = true; + }; + + this.element.addEventListener('load', listener); + this.element.addEventListener('error', listener); + } } diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index ea7b02b9a02..552ca920342 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -464,6 +464,11 @@ export class CoreFormatTextDirective implements OnChanges { let promise: Promise = null; if (externalImages.length) { promise = Promise.all(externalImages.map((externalImage) => { + if (externalImage.loaded) { + // Image has already been loaded, no need to wait. + return Promise.resolve(); + } + return new Promise((resolve): void => { const subscription = externalImage.onLoad.subscribe(() => { subscription.unsubscribe(); From 0e1bb20c9e0cfe52301ba7503b322e97ebcddb2b Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 26 Aug 2019 08:32:40 +0200 Subject: [PATCH 212/241] MOBILE-3068 core: Fix cannot read indexOf null in external-content --- src/directives/external-content.ts | 2 +- src/directives/format-text.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/directives/external-content.ts b/src/directives/external-content.ts index fac78ee1730..e70858ab0b0 100644 --- a/src/directives/external-content.ts +++ b/src/directives/external-content.ts @@ -150,7 +150,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges { } // Avoid handling data url's. - if (url.indexOf('data:') === 0) { + if (url && url.indexOf('data:') === 0) { this.invalid = true; return; diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index 1335b31decb..e8bafbbab57 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -468,7 +468,7 @@ export class CoreFormatTextDirective implements OnChanges { // Wait for images to load. let promise: Promise = null; if (externalImages.length) { - promise = Promise.all(externalImages.map((externalImage) => { + promise = Promise.all(externalImages.map((externalImage): any => { if (externalImage.loaded) { // Image has already been loaded, no need to wait. return Promise.resolve(); From f3debfab1fd8cc360bd6a9ace9a1d0d856607e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 26 Aug 2019 10:12:48 +0200 Subject: [PATCH 213/241] MOBILE-3068 ionic: Close modals before going back --- src/app/app.component.ts | 13 +++++++++++++ src/app/app.scss | 5 +++++ src/components/context-menu/context-menu.ts | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index cd0200f8d2d..96c56a18ab1 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -65,6 +65,19 @@ export class MoodleMobileApp implements OnInit { app.setElementClass('platform-windows', true); } } + + // Register back button action to allow closing modals before anything else. + this.appProvider.registerBackButtonAction(() => { + // Following function is hidden in Ionic Code, however there's no solution for that. + const portal = app._getActivePortal(); + if (portal) { + portal.pop(); + + return true; + } + + return false; + }, 2000); }); } diff --git a/src/app/app.scss b/src/app/app.scss index 9fbf324f0ec..52925703726 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -1012,6 +1012,11 @@ ion-app.app-root { ion-alert .alert-checkbox-button .alert-checkbox-label { white-space: normal; } + + ion-backdrop { + transition: opacity 100ms ease-in-out; + opacity: .1; + } } @each $color-name, $color-base, $color-contrast in get-colors($colors) { diff --git a/src/components/context-menu/context-menu.ts b/src/components/context-menu/context-menu.ts index ff6413b6773..42fca4504a9 100644 --- a/src/components/context-menu/context-menu.ts +++ b/src/components/context-menu/context-menu.ts @@ -178,7 +178,7 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { showContextMenu(event: MouseEvent): void { if (!this.expanded) { const popover = this.popoverCtrl.create(CoreContextMenuPopoverComponent, - { title: this.title, items: this.items, id: this.uniqueId }); + { title: this.title, items: this.items, id: this.uniqueId, showBackdrop: true }); popover.onDidDismiss(() => { this.expanded = false; From 330f1741f942c8f7ddd9791f5e4881e59257a930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 26 Aug 2019 11:46:20 +0200 Subject: [PATCH 214/241] MOBILE-3068 course: Check action is avalaible before navigating --- src/core/course/providers/helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index 36738cff8f5..73d30973c09 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -1170,7 +1170,7 @@ export class CoreCourseHelperProvider { module.handlerData = this.moduleDelegate.getModuleDataFor(module.modname, module, courseId, sectionId); - if (navCtrl) { + if (navCtrl && module.handlerData && module.handlerData.action) { // If the link handler for this module passed through navCtrl, we can use the module's handler to navigate cleanly. // Otherwise, we will redirect below. modal.dismiss(); From 991e170fd0a6c3fe7199ab6f3d61ebd6ee4020b7 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 26 Aug 2019 12:26:24 +0200 Subject: [PATCH 215/241] MOBILE-3068 core: Fix not an object error in newTab.enabled --- src/components/tabs/tabs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index bf1b3c6cdb9..ba7b9fdb15f 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -488,7 +488,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe const currentTab = this.getSelected(), newTab = this.tabs[index]; - if (!newTab.enabled || !newTab.show) { + if (!newTab || !newTab.enabled || !newTab.show) { // The tab isn't enabled or shown, stop. return; } From 06f4b0ed4832db4555413bc959b00baad872d049 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 26 Aug 2019 16:29:06 +0200 Subject: [PATCH 216/241] MOBILE-3068 glossary: Prefetch categories --- src/addon/mod/glossary/providers/prefetch-handler.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/addon/mod/glossary/providers/prefetch-handler.ts b/src/addon/mod/glossary/providers/prefetch-handler.ts index 47369498e6d..9235c4b54e5 100644 --- a/src/addon/mod/glossary/providers/prefetch-handler.ts +++ b/src/addon/mod/glossary/providers/prefetch-handler.ts @@ -179,6 +179,9 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH return Promise.all(promises); })); + // Get all categories. + promises.push(this.glossaryProvider.getAllCategories(glossary.id)); + // Prefetch data for link handlers. promises.push(this.courseProvider.getModuleBasicInfo(module.id, siteId)); promises.push(this.courseProvider.getModuleBasicInfoByInstance(glossary.id, 'glossary', siteId)); From 2c63478f47d35aa5419b32656cffc389a02bff2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 26 Aug 2019 15:20:51 +0200 Subject: [PATCH 217/241] MOBILE-3068 core: Decode URL params on links handlers --- src/addon/mod/wiki/providers/edit-link-handler.ts | 2 +- src/providers/utils/url.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/addon/mod/wiki/providers/edit-link-handler.ts b/src/addon/mod/wiki/providers/edit-link-handler.ts index 5c74cb4d75b..512c8c65f3d 100644 --- a/src/addon/mod/wiki/providers/edit-link-handler.ts +++ b/src/addon/mod/wiki/providers/edit-link-handler.ts @@ -50,7 +50,7 @@ export class AddonModWikiEditLinkHandler extends CoreContentLinksHandlerBase { let section = ''; if (typeof params.section != 'undefined') { - section = this.textUtils.decodeURIComponent(params.section.replace(/\+/g, ' ')); + section = params.section.replace(/\+/g, ' '); } const pageParams = { diff --git a/src/providers/utils/url.ts b/src/providers/utils/url.ts index 70bd15d55e1..a7b47de344b 100644 --- a/src/providers/utils/url.ts +++ b/src/providers/utils/url.ts @@ -79,7 +79,7 @@ export class CoreUrlUtilsProvider { } urlAndHash[0].replace(regex, (match: string, key: string, value: string): string => { - params[key] = typeof value != 'undefined' ? value : ''; + params[key] = typeof value != 'undefined' ? this.textUtils.decodeURIComponent(value) : ''; if (subParams) { params[key] = params[key].replace(subParamsPlaceholder, subParams); From f0b53afe577f345010487ecd80276ddec5744440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 26 Aug 2019 15:48:23 +0200 Subject: [PATCH 218/241] MOBILE-3068 styles: Multiple select width to 100% --- src/addon/mod/glossary/pages/edit/edit.html | 2 +- src/app/app.scss | 16 ++++++++++++---- src/theme/variables.scss | 20 ++++++++++++++++---- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/addon/mod/glossary/pages/edit/edit.html b/src/addon/mod/glossary/pages/edit/edit.html index f5c2381a0e7..5678fcba899 100644 --- a/src/addon/mod/glossary/pages/edit/edit.html +++ b/src/addon/mod/glossary/pages/edit/edit.html @@ -19,7 +19,7 @@ {{ 'addon.mod_glossary.categories' | translate }} - + {{ category.name }} diff --git a/src/app/app.scss b/src/app/app.scss index 52925703726..811d7724cd7 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -388,11 +388,11 @@ ion-app.app-root { ion-select { position: relative; // Ionic fix. Button can occupy all page if not. - color: $core-select-placeholder-color; + color: $core-select-color; align-self: start; .select-icon .select-icon-inner { - color: $core-select-placeholder-color; + color: $core-select-color; } &.select-disabled .select-icon .select-icon-inner { @@ -411,10 +411,18 @@ ion-app.app-root { } } + .item-label-stacked ion-select[multiple="true"] { + width: 100%; + } + + ion-select .select-placeholder { + color: $core-select-placeholder-color; + } + ion-select.core-button-select, .core-button-select { background-color: white; - color: $core-select-placeholder-color; + color: $core-select-color; white-space: normal; align-self: start; max-width: none; @@ -446,7 +454,7 @@ ion-app.app-root { } .select-icon .select-icon-inner { - color: $core-select-placeholder-color; + color: $core-select-color; } ion-icon:last-child { diff --git a/src/theme/variables.scss b/src/theme/variables.scss index b2ac149d154..0cd36e33e51 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -131,7 +131,16 @@ $refresher-icon-color: $core-color !default; $core-online-color: #5cb85c; -$core-select-placeholder-color: $core-color !default; +$core-placeholder-color: $gray-dark !default; +$core-select-placeholder-color: $core-placeholder-color !default; +$alert-input-placeholder-color: $core-placeholder-color !default; +$core-datetime-ios-placeholder-color: $core-placeholder-color !default; +$searchbar-ios-input-placeholder-color: $core-placeholder-color !default; +$searchbar-md-input-placeholder-color: $core-placeholder-color !default; +$searchbar-wp-input-placeholder-color: $core-placeholder-color !default; +$text-input-placeholder-color: $core-placeholder-color !default; + +$core-select-color: $core-color !default; $item-avatar-size: 54px !default; $input-select-opacity: .5 !default; $note-color: $gray-dark !default; @@ -179,7 +188,8 @@ $tabs-ios-tab-color-inactive: $tabs-tab-color-inactive; $button-ios-outline-background-color: $core-button-outline-background-color; $toolbar-ios-height: 44px + 8; // Avoid toolbar with different heights. $checkbox-ios-icon-border-radius: 0px !default; -$select-ios-placeholder-color: $core-select-placeholder-color; +$select-ios-placeholder-color: $core-select-color !default; +$datetime-ios-placeholder-color: $core-datetime-ios-placeholder-color; $radio-ios-disabled-opacity: $input-select-opacity !default; $checkbox-ios-disabled-opacity: $input-select-opacity !default; $toggle-ios-disabled-opacity: $input-select-opacity !default; @@ -203,7 +213,8 @@ $spinner-md-crescent-color: $core-spinner-color; $tabs-md-tab-color-inactive: $tabs-tab-color-inactive; $button-md-outline-background-color: $core-button-outline-background-color; $font-family-md-base: "Roboto", "Noto Sans", "Helvetica Neue", sans-serif !default; -$select-md-placeholder-color: $core-select-placeholder-color; +$select-md-placeholder-color: $core-select-color !default; +$datetime-md-placeholder-color: $core-datetime-ios-placeholder-color !default; $label-md-text-color: $text-color !default; $radio-md-disabled-opacity: $input-select-opacity !default; $checkbox-md-disabled-opacity: $input-select-opacity !default; @@ -226,7 +237,8 @@ $loading-wp-spinner-color: $core-loading-spinner-color; $spinner-wp-circles-color: $core-spinner-color; $tabs-wp-tab-color-inactive: $tabs-tab-color-inactive; $button-wp-outline-background-color: $core-button-outline-background-color; -$select-wp-placeholder-color: $core-select-placeholder-color; +$select-wp-placeholder-color: $core-select-color !default; +$datetime-wp-placeholder-color: $core-datetime-ios-placeholder-color !default; $label-wp-text-color: $text-color !default; $radio-wp-disabled-opacity: $input-select-opacity !default; $checkbox-wp-disabled-opacity: $input-select-opacity !default; From ebd4577be26338b5617fb7062a5cd35396f5a195 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 27 Aug 2019 09:27:13 +0200 Subject: [PATCH 219/241] MOBILE-3068 core: Fix img loaded event if no URL --- src/directives/external-content.ts | 16 +++++++++++----- src/directives/format-text.ts | 5 +++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/directives/external-content.ts b/src/directives/external-content.ts index e70858ab0b0..4026e7e0a25 100644 --- a/src/directives/external-content.ts +++ b/src/directives/external-content.ts @@ -152,12 +152,22 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges { // Avoid handling data url's. if (url && url.indexOf('data:') === 0) { this.invalid = true; + this.onLoad.emit(); + this.loaded = true; return; } this.handleExternalContent(targetAttr, url, siteId).catch(() => { - // Ignore errors. + // Error handling content. Make sure the loaded event is triggered for images. + if (tagName === 'IMG') { + if (url) { + this.waitForLoad(); + } else { + this.onLoad.emit(); + this.loaded = true; + } + } }); } @@ -204,10 +214,6 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges { this.addSource(url); } - if (tagName === 'IMG') { - this.waitForLoad(); - } - return Promise.reject(null); } diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index e8bafbbab57..ede668a6b73 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -468,7 +468,8 @@ export class CoreFormatTextDirective implements OnChanges { // Wait for images to load. let promise: Promise = null; if (externalImages.length) { - promise = Promise.all(externalImages.map((externalImage): any => { + // Automatically reject the promise after 5 seconds to prevent blocking the user forever. + promise = this.utils.timeoutPromise(this.utils.allPromises(externalImages.map((externalImage): any => { if (externalImage.loaded) { // Image has already been loaded, no need to wait. return Promise.resolve(); @@ -480,7 +481,7 @@ export class CoreFormatTextDirective implements OnChanges { resolve(); }); }); - })); + })), 5000); } else { promise = Promise.resolve(); } From 265f15f3801fae651db95cd34f600921de266315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 27 Aug 2019 09:38:59 +0200 Subject: [PATCH 220/241] MOBILE-3068 quiz: Close modals when time is up --- src/addon/mod/lesson/pages/player/player.ts | 11 +++++++- src/addon/mod/quiz/pages/player/player.ts | 10 ++++++- src/app/app.component.ts | 31 +++++++++++++-------- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/addon/mod/lesson/pages/player/player.ts b/src/addon/mod/lesson/pages/player/player.ts index ab32eb9d670..a19c4e48fcc 100644 --- a/src/addon/mod/lesson/pages/player/player.ts +++ b/src/addon/mod/lesson/pages/player/player.ts @@ -25,6 +25,7 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; +import { MoodleMobileApp } from '../../../../../app/app.component'; import { AddonModLessonProvider } from '../../providers/lesson'; import { AddonModLessonOfflineProvider } from '../../providers/lesson-offline'; import { AddonModLessonSyncProvider } from '../../providers/lesson-sync'; @@ -85,7 +86,8 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { protected lessonHelper: AddonModLessonHelperProvider, protected lessonSync: AddonModLessonSyncProvider, protected lessonOfflineProvider: AddonModLessonOfflineProvider, protected cdr: ChangeDetectorRef, modalCtrl: ModalController, protected navCtrl: NavController, protected appProvider: CoreAppProvider, - protected utils: CoreUtilsProvider, protected urlUtils: CoreUrlUtilsProvider, protected fb: FormBuilder) { + protected utils: CoreUtilsProvider, protected urlUtils: CoreUrlUtilsProvider, protected fb: FormBuilder, + protected mmApp: MoodleMobileApp) { this.lessonId = navParams.get('lessonId'); this.courseId = navParams.get('courseId'); @@ -145,6 +147,13 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { return Promise.resolve(); } + /** + * Runs when the page is about to leave and no longer be the active page. + */ + ionViewWillLeave(): void { + this.mmApp.closeModal(); + } + /** * A button was clicked. * diff --git a/src/addon/mod/quiz/pages/player/player.ts b/src/addon/mod/quiz/pages/player/player.ts index 75da900d873..af65f378a99 100644 --- a/src/addon/mod/quiz/pages/player/player.ts +++ b/src/addon/mod/quiz/pages/player/player.ts @@ -23,6 +23,7 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; import { CoreQuestionComponent } from '@core/question/components/question/question'; +import { MoodleMobileApp } from '../../../../../app/app.component'; import { AddonModQuizProvider } from '../../providers/quiz'; import { AddonModQuizSyncProvider } from '../../providers/quiz-sync'; import { AddonModQuizHelperProvider } from '../../providers/helper'; @@ -80,7 +81,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { protected timeUtils: CoreTimeUtilsProvider, protected quizProvider: AddonModQuizProvider, protected quizHelper: AddonModQuizHelperProvider, protected quizSync: AddonModQuizSyncProvider, protected questionHelper: CoreQuestionHelperProvider, protected cdr: ChangeDetectorRef, - modalCtrl: ModalController, protected navCtrl: NavController) { + modalCtrl: ModalController, protected navCtrl: NavController, protected mmApp: MoodleMobileApp) { this.quizId = navParams.get('quizId'); this.courseId = navParams.get('courseId'); @@ -157,6 +158,13 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { return Promise.resolve(); } + /** + * Runs when the page is about to leave and no longer be the active page. + */ + ionViewWillLeave(): void { + this.mmApp.closeModal(); + } + /** * Abort the quiz. */ diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 96c56a18ab1..7df4951b2a3 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -38,10 +38,10 @@ export class MoodleMobileApp implements OnInit { protected lastUrls = {}; protected lastInAppUrl: string; - constructor(private platform: Platform, logger: CoreLoggerProvider, keyboard: Keyboard, + constructor(private platform: Platform, logger: CoreLoggerProvider, keyboard: Keyboard, private app: IonicApp, private eventsProvider: CoreEventsProvider, private loginHelper: CoreLoginHelperProvider, private zone: NgZone, private appProvider: CoreAppProvider, private langProvider: CoreLangProvider, private sitesProvider: CoreSitesProvider, - private screenOrientation: ScreenOrientation, app: IonicApp, private urlSchemesProvider: CoreCustomURLSchemesProvider, + private screenOrientation: ScreenOrientation, private urlSchemesProvider: CoreCustomURLSchemesProvider, private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider) { this.logger = logger.getInstance('AppComponent'); @@ -68,15 +68,7 @@ export class MoodleMobileApp implements OnInit { // Register back button action to allow closing modals before anything else. this.appProvider.registerBackButtonAction(() => { - // Following function is hidden in Ionic Code, however there's no solution for that. - const portal = app._getActivePortal(); - if (portal) { - portal.pop(); - - return true; - } - - return false; + return this.closeModal(); }, 2000); }); @@ -302,4 +294,21 @@ export class MoodleMobileApp implements OnInit { document.body.classList.remove(tempClass); }); } + + /** + * Close one modal if any. + * + * @return {boolean} True if one modal was present. + */ + closeModal(): boolean { + // Following function is hidden in Ionic Code, however there's no solution for that. + const portal = this.app._getActivePortal(); + if (portal) { + portal.pop(); + + return true; + } + + return false; + } } From 886880a2f0b1ca0bd02b3349f285b97f945c62f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 27 Aug 2019 09:39:17 +0200 Subject: [PATCH 221/241] MOBILE-3068 rte: Update toolbar arrows every click --- src/components/rich-text-editor/rich-text-editor.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/rich-text-editor/rich-text-editor.ts b/src/components/rich-text-editor/rich-text-editor.ts index 08bf278b356..e9c8bc2b535 100644 --- a/src/components/rich-text-editor/rich-text-editor.ts +++ b/src/components/rich-text-editor/rich-text-editor.ts @@ -585,6 +585,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy const currentIndex = this.toolbarSlides.getActiveIndex() || 0; this.toolbarSlides.slideTo(currentIndex + this.numToolbarButtons); } + this.updateToolbarArrows(); } /** @@ -597,6 +598,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy const currentIndex = this.toolbarSlides.getActiveIndex() || 0; this.toolbarSlides.slideTo(currentIndex - this.numToolbarButtons); } + this.updateToolbarArrows(); } /** From 3da30f665a9df6d628822c99a2fbcf5a2f84eb10 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 27 Aug 2019 16:13:41 +0200 Subject: [PATCH 222/241] MOBILE-3068 utils: Fix max stack size reached when cloning --- src/addon/mod/data/providers/helper.ts | 2 +- src/providers/utils/utils.ts | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts index d5be5fa199d..a9e52775bc4 100644 --- a/src/addon/mod/data/providers/helper.ts +++ b/src/addon/mod/data/providers/helper.ts @@ -337,7 +337,7 @@ export class AddonModDataHelperProvider { approved: !data.approval || data.manageapproved, canmanageentry: true, fullname: site.getInfo().fullname, - contents: [], + contents: {}, } }); } diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index db08cf476fc..bc73ce6ca47 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -58,6 +58,7 @@ export interface PromiseDefer { */ @Injectable() export class CoreUtilsProvider { + protected DONT_CLONE = ['[object FileEntry]', '[object DirectoryEntry]', '[object DOMFileSystem]']; protected logger; protected iabInstance: InAppBrowserObject; protected uniqueIds: {[name: string]: number} = {}; @@ -267,22 +268,36 @@ export class CoreUtilsProvider { * Clone a variable. It should be an object, array or primitive type. * * @param {any} source The variable to clone. + * @param {number} [level=0] Depth we are right now inside a cloned object. It's used to prevent reaching max call stack size. * @return {any} Cloned variable. */ - clone(source: any): any { + clone(source: any, level: number = 0): any { + if (level >= 20) { + // Max 20 levels. + this.logger.error('Max depth reached when cloning object.', source); + + return source; + } + if (Array.isArray(source)) { // Clone the array and all the entries. const newArray = []; for (let i = 0; i < source.length; i++) { - newArray[i] = this.clone(source[i]); + newArray[i] = this.clone(source[i], level + 1); } return newArray; } else if (typeof source == 'object' && source !== null) { + // Check if the object shouldn't be copied. + if (source && source.toString && this.DONT_CLONE.indexOf(source.toString()) != -1) { + // Object shouldn't be copied, return it as it is. + return source; + } + // Clone the object and all the subproperties. const newObject = {}; for (const name in source) { - newObject[name] = this.clone(source[name]); + newObject[name] = this.clone(source[name], level + 1); } return newObject; From a604b1d6a04a4608d8f0a00ca3fc2d362c522798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 27 Aug 2019 16:28:50 +0200 Subject: [PATCH 223/241] MOBILE-3068 splitview: Force only one push at a time --- src/components/split-view/split-view.ts | 36 ++++++++++++++++--------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/components/split-view/split-view.ts b/src/components/split-view/split-view.ts index 3e26f6e8672..ca807f10dac 100644 --- a/src/components/split-view/split-view.ts +++ b/src/components/split-view/split-view.ts @@ -60,6 +60,7 @@ export class CoreSplitViewComponent implements OnInit, OnDestroy { protected ignoreSplitChanged = false; protected audioCaptureSubscription: Subscription; protected languageChangedSubscription: Subscription; + protected pushOngoing: boolean; // Empty placeholder for the 'detail' page. detailPage: any = null; @@ -185,20 +186,29 @@ export class CoreSplitViewComponent implements OnInit, OnDestroy { * @param {boolean} [retrying] Whether it's retrying. */ push(page: any, params?: any, retrying?: boolean): void { - if (typeof this.isEnabled == 'undefined' && !retrying) { - // Hasn't calculated if it's enabled yet. Wait a bit and try again. - setTimeout(() => { - this.push(page, params, true); - }, 200); - } else { - if (this.isEnabled) { - this.detailNav.setRoot(page, params); + // Check there's no ongoing push. + if (!this.pushOngoing) { + if (typeof this.isEnabled == 'undefined' && !retrying) { + // Hasn't calculated if it's enabled yet. Wait a bit and try again. + setTimeout(() => { + this.push(page, params, true); + }, 200); } else { - this.loadDetailPage = { - component: page, - data: params - }; - this.masterNav.push(page, params); + this.pushOngoing = true; + let promise; + + if (this.isEnabled) { + promise = this.detailNav.setRoot(page, params); + } else { + this.loadDetailPage = { + component: page, + data: params + }; + promise = this.masterNav.push(page, params); + } + promise.finally(() => { + this.pushOngoing = false; + }); } } } From 160b544d8e75d8a23b0d28381055151f78eca73f Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 27 Aug 2019 16:53:32 +0200 Subject: [PATCH 224/241] MOBILE-3068 database: Fix # displayed in menu fields in offline --- src/addon/mod/data/fields/multimenu/providers/handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addon/mod/data/fields/multimenu/providers/handler.ts b/src/addon/mod/data/fields/multimenu/providers/handler.ts index 7c98f1b8c03..0522bc30478 100644 --- a/src/addon/mod/data/fields/multimenu/providers/handler.ts +++ b/src/addon/mod/data/fields/multimenu/providers/handler.ts @@ -126,7 +126,7 @@ export class AddonModDataFieldMultimenuHandler implements AddonModDataFieldHandl * @return {any} Data overriden */ overrideData(originalContent: any, offlineContent: any, offlineFiles?: any): any { - originalContent.content = (offlineContent[''] && offlineContent[''].join('###')) || ''; + originalContent.content = (offlineContent[''] && offlineContent[''].join('##')) || ''; return originalContent; } From 5533c39288706def9fd5f9d0881fde9eb5eb8a0f Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 28 Aug 2019 10:38:44 +0200 Subject: [PATCH 225/241] MOBILE-3068 database: Fix comments not updated on PTR --- src/addon/mod/data/components/index/index.ts | 9 ++-- src/addon/mod/data/pages/entry/entry.ts | 13 +++-- src/addon/mod/data/providers/helper.ts | 9 ++-- .../comments/components/comments/comments.ts | 52 ++++++++++++++++++- src/core/comments/providers/comments.ts | 2 + 5 files changed, 73 insertions(+), 12 deletions(-) diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts index ae91044aeac..bc346384205 100644 --- a/src/addon/mod/data/components/index/index.ts +++ b/src/addon/mod/data/components/index/index.ts @@ -85,7 +85,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp private prefetchHandler: AddonModDataPrefetchHandler, private timeUtils: CoreTimeUtilsProvider, private groupsProvider: CoreGroupsProvider, - private commentsProvider: CoreCommentsProvider, private modalCtrl: ModalController, private utils: CoreUtilsProvider, protected navCtrl: NavController) { @@ -152,8 +151,12 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp promises.push(this.dataProvider.invalidateDatabaseAccessInformationData(this.data.id)); promises.push(this.groupsProvider.invalidateActivityGroupInfo(this.data.coursemodule)); promises.push(this.dataProvider.invalidateEntriesData(this.data.id)); + if (this.hasComments) { - promises.push(this.commentsProvider.invalidateCommentsByInstance('module', this.data.coursemodule)); + this.eventsProvider.trigger(CoreCommentsProvider.REFRESH_COMMENTS_EVENT, { + contextLevel: 'module', + instanceId: this.data.coursemodule + }, this.sitesProvider.getCurrentSiteId()); } } @@ -192,6 +195,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => { this.data = data; + this.hasComments = data.comments; this.description = data.intro || data.description; this.dataRetrieved.emit(data); @@ -258,7 +262,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp * @return {Promise} Resolved then done. */ protected fetchEntriesData(): Promise { - this.hasComments = false; return this.dataProvider.getDatabaseAccessInformation(this.data.id, this.selectedGroup).then((accessData) => { // Update values for current group. diff --git a/src/addon/mod/data/pages/entry/entry.ts b/src/addon/mod/data/pages/entry/entry.ts index e486cdf5f97..95f96d8fca4 100644 --- a/src/addon/mod/data/pages/entry/entry.ts +++ b/src/addon/mod/data/pages/entry/entry.ts @@ -27,6 +27,7 @@ import { AddonModDataSyncProvider } from '../../providers/sync'; import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; import { AddonModDataComponentsModule } from '../../components/components.module'; import { CoreCommentsProvider } from '@core/comments/providers/comments'; +import { CoreCommentsCommentsComponent } from '@core/comments/components/comments/comments'; /** * Page that displays the view entry page. @@ -38,6 +39,7 @@ import { CoreCommentsProvider } from '@core/comments/providers/comments'; }) export class AddonModDataEntryPage implements OnDestroy { @ViewChild(Content) content: Content; + @ViewChild(CoreCommentsCommentsComponent) comments: CoreCommentsCommentsComponent; protected module: any; protected entryId: number; @@ -211,13 +213,16 @@ export class AddonModDataEntryPage implements OnDestroy { promises.push(this.dataProvider.invalidateDatabaseData(this.courseId)); if (this.data) { - if (this.data.comments && this.entry && this.entry.id > 0 && this.commentsEnabled) { - promises.push(this.commentsProvider.invalidateCommentsData('module', this.data.coursemodule, 'mod_data', - this.entry.id, 'database_entry')); - } promises.push(this.dataProvider.invalidateEntryData(this.data.id, this.entryId)); promises.push(this.groupsProvider.invalidateActivityGroupInfo(this.data.coursemodule)); promises.push(this.dataProvider.invalidateEntriesData(this.data.id)); + + if (this.data.comments && this.entry && this.entry.id > 0 && this.commentsEnabled && this.comments) { + // Refresh comments. Don't add it to promises because we don't want the comments fetch to block the entry fetch. + this.comments.doRefresh().catch(() => { + // Ignore errors. + }); + } } return Promise.all(promises).finally(() => { diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts index a9e52775bc4..2938c6c7281 100644 --- a/src/addon/mod/data/providers/helper.ts +++ b/src/addon/mod/data/providers/helper.ts @@ -158,11 +158,12 @@ export class AddonModDataHelperProvider { * @param {any} entry Entry. * @param {number} offset Entry offset. * @param {string} mode Mode list or show. - * @param {AddonModDataOfflineAction[]} actions Actions that can be performed to the record. + * @param {{[name: string]: boolean}} actions Actions that can be performed to the record. * @return {string} Generated HTML. */ displayShowFields(template: string, fields: any[], entry: any, offset: number, mode: string, - actions: AddonModDataOfflineAction[]): string { + actions: {[name: string]: boolean}): string { + if (!template) { return ''; } @@ -357,9 +358,9 @@ export class AddonModDataHelperProvider { * @param {any} database Database activity. * @param {any} accessInfo Access info to the activity. * @param {any} record Entry or record where the actions will be performed. - * @return {any} Keyed with the action names and boolean to evalute if it can or cannot be done. + * @return {{[name: string]: boolean}} Keyed with the action names and boolean to evalute if it can or cannot be done. */ - getActions(database: any, accessInfo: any, record: any): any { + getActions(database: any, accessInfo: any, record: any): {[name: string]: boolean} { return { more: true, moreurl: true, diff --git a/src/core/comments/components/comments/comments.ts b/src/core/comments/components/comments/comments.ts index 6f7a2244479..6e32bc71034 100644 --- a/src/core/comments/components/comments/comments.ts +++ b/src/core/comments/components/comments/comments.ts @@ -41,6 +41,7 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { disabled = false; protected updateSiteObserver; + protected refreshCommentsObserver; constructor(private navCtrl: NavController, private commentsProvider: CoreCommentsProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider) { @@ -58,6 +59,19 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { this.fetchData(); } }, sitesProvider.getCurrentSiteId()); + + // Refresh comments if event received. + this.refreshCommentsObserver = eventsProvider.on(CoreCommentsProvider.REFRESH_COMMENTS_EVENT, (data) => { + // Verify these comments need to be updated. + if (this.undefinedOrEqual(data, 'contextLevel') && this.undefinedOrEqual(data, 'instanceId') && + this.undefinedOrEqual(data, 'component') && this.undefinedOrEqual(data, 'itemId') && + this.undefinedOrEqual(data, 'area')) { + + this.doRefresh().catch(() => { + // Ignore errors. + }); + } + }, sitesProvider.getCurrentSiteId()); } /** @@ -77,7 +91,10 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { } } - protected fetchData(): void { + /** + * Fetch comments data. + */ + fetchData(): void { if (this.disabled) { return; } @@ -94,6 +111,27 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { }); } + /** + * Refresh comments. + * + * @return {Promise} Promise resolved when done. + */ + doRefresh(): Promise { + return this.invalidateComments().then(() => { + return this.fetchData(); + }); + } + + /** + * Invalidate comments data. + * + * @return {Promise} Promise resolved when done. + */ + invalidateComments(): Promise { + return this.commentsProvider.invalidateCommentsData(this.contextLevel, this.instanceId, this.component, this.itemId, + this.area); + } + /** * Opens the comments page. */ @@ -116,5 +154,17 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { */ ngOnDestroy(): void { this.updateSiteObserver && this.updateSiteObserver.off(); + this.refreshCommentsObserver && this.refreshCommentsObserver.off(); + } + + /** + * Check if a certain value in data is undefined or equal to this instance value. + * + * @param {any} data Data object. + * @param {string} name Name of the property to check. + * @return {boolean} Whether it's undefined or equal. + */ + protected undefinedOrEqual(data: any, name: string): boolean { + return typeof data[name] == 'undefined' || data[name] == this[name]; } } diff --git a/src/core/comments/providers/comments.ts b/src/core/comments/providers/comments.ts index eb5613c4584..16d7c8f9527 100644 --- a/src/core/comments/providers/comments.ts +++ b/src/core/comments/providers/comments.ts @@ -25,6 +25,8 @@ import { CoreCommentsOfflineProvider } from './offline'; @Injectable() export class CoreCommentsProvider { + static REFRESH_COMMENTS_EVENT = 'core_comments_refresh_comments'; + protected ROOT_CACHE_KEY = 'mmComments:'; static pageSize = null; static pageSizeOK = false; // If true, the pageSize is definitive. If not, it's a temporal value to reduce WS calls. From 5794bade90fdf718fca15834676e49461b961ed6 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 28 Aug 2019 10:52:06 +0200 Subject: [PATCH 226/241] MOBILE-3068 blog: Fix entries disappearing on PTR --- src/addon/blog/components/entries/entries.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addon/blog/components/entries/entries.ts b/src/addon/blog/components/entries/entries.ts index 89a5b091770..aa66beaef40 100644 --- a/src/addon/blog/components/entries/entries.ts +++ b/src/addon/blog/components/entries/entries.ts @@ -223,7 +223,7 @@ export class AddonBlogEntriesComponent implements OnInit { this.filter['userid'] = this.currentUserId; promises.push(this.blogProvider.invalidateEntries(this.filter)); - if (!this.showMyEntriesToggle) { + if (!this.onlyMyEntries) { delete this.filter['userid']; } From 43d6839a8d78d7570e9c84044f86c780a583f5ec Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 28 Aug 2019 11:20:07 +0200 Subject: [PATCH 227/241] MOBILE-3068 comments: Open comments in new page if split view --- .../addon-mod-assign-submission-comments.html | 2 +- .../submission/comments/component/comments.ts | 4 ++-- .../comments/components/comments/comments.ts | 18 ++++++++++++++---- .../components/comments/core-comments.html | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/addon/mod/assign/submission/comments/component/addon-mod-assign-submission-comments.html b/src/addon/mod/assign/submission/comments/component/addon-mod-assign-submission-comments.html index be2ba466f32..8040cdfeb15 100644 --- a/src/addon/mod/assign/submission/comments/component/addon-mod-assign-submission-comments.html +++ b/src/addon/mod/assign/submission/comments/component/addon-mod-assign-submission-comments.html @@ -1,4 +1,4 @@ - +

{{plugin.name}}

diff --git a/src/addon/mod/assign/submission/comments/component/comments.ts b/src/addon/mod/assign/submission/comments/component/comments.ts index 98c8be9e0fd..bd6bb57c2b6 100644 --- a/src/addon/mod/assign/submission/comments/component/comments.ts +++ b/src/addon/mod/assign/submission/comments/component/comments.ts @@ -48,7 +48,7 @@ export class AddonModAssignSubmissionCommentsComponent extends AddonModAssignSub /** * Show the comments. */ - showComments(): void { - this.commentsComponent && this.commentsComponent.openComments(); + showComments(e?: Event): void { + this.commentsComponent && this.commentsComponent.openComments(e); } } diff --git a/src/core/comments/components/comments/comments.ts b/src/core/comments/components/comments/comments.ts index 6e32bc71034..6bbe34fc45d 100644 --- a/src/core/comments/components/comments/comments.ts +++ b/src/core/comments/components/comments/comments.ts @@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output, SimpleChange } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output, SimpleChange, Optional } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreCommentsProvider } from '../../providers/comments'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; /** * Component that displays the count of comments. @@ -44,7 +45,9 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { protected refreshCommentsObserver; constructor(private navCtrl: NavController, private commentsProvider: CoreCommentsProvider, - sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider) { + sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, + @Optional() private svComponent: CoreSplitViewComponent) { + this.onLoading = new EventEmitter(); this.disabled = this.commentsProvider.areCommentsDisabledInSite(); @@ -135,10 +138,17 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy { /** * Opens the comments page. */ - openComments(): void { + openComments(e?: Event): void { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } + if (!this.disabled && !this.countError) { // Open a new state with the interpolated contents. - this.navCtrl.push('CoreCommentsViewerPage', { + const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; + + navCtrl.push('CoreCommentsViewerPage', { contextLevel: this.contextLevel, instanceId: this.instanceId, componentName: this.component, diff --git a/src/core/comments/components/comments/core-comments.html b/src/core/comments/components/comments/core-comments.html index 2c2c8efebcc..4375edc5ace 100644 --- a/src/core/comments/components/comments/core-comments.html +++ b/src/core/comments/components/comments/core-comments.html @@ -1,5 +1,5 @@ -
+
{{ 'core.comments.commentscount' | translate : {'$a': commentsCount} }}
From 3e29f2e4280ad4e228dc1003beacf4d5e16810e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 28 Aug 2019 09:32:07 +0200 Subject: [PATCH 228/241] MOBILE-3068 loading: Fix loading styles on blocks and tabs --- .../addon-mod-assign-submission.html | 2 +- src/components/empty-box/empty-box.scss | 5 +++++ src/components/loading/loading.scss | 2 ++ .../components/format/core-course-format.html | 22 +++++++++---------- .../core-siteplugins-plugin-content.html | 2 +- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html b/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html index c46b8942ece..06bebe6cb40 100644 --- a/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html +++ b/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html @@ -1,4 +1,4 @@ - + diff --git a/src/components/empty-box/empty-box.scss b/src/components/empty-box/empty-box.scss index 5d265575c55..e5d0c3edb0d 100644 --- a/src/components/empty-box/empty-box.scss +++ b/src/components/empty-box/empty-box.scss @@ -61,3 +61,8 @@ ion-app.app-root core-empty-box { } } } + +ion-app.app-root core-block-course-blocks core-empty-box .core-empty-box { + position: relative; +} + diff --git a/src/components/loading/loading.scss b/src/components/loading/loading.scss index 92f00180552..bc1faec9fe3 100644 --- a/src/components/loading/loading.scss +++ b/src/components/loading/loading.scss @@ -21,12 +21,14 @@ ion-app.app-root { .scroll-content > core-loading, ion-content > .scroll-content > core-loading, + core-tab core-loading, .core-loading-center { position: static !important; } .scroll-content > core-loading, ion-content > .scroll-content > core-loading, + core-tab core-loading, .core-loading-center, core-loading.core-loading-loaded { position: relative; diff --git a/src/core/course/components/format/core-course-format.html b/src/core/course/components/format/core-course-format.html index 2d01ef5a1ab..c7c3bf6ed96 100644 --- a/src/core/course/components/format/core-course-format.html +++ b/src/core/course/components/format/core-course-format.html @@ -54,18 +54,18 @@
- - - - - + + + + + diff --git a/src/core/siteplugins/components/plugin-content/core-siteplugins-plugin-content.html b/src/core/siteplugins/components/plugin-content/core-siteplugins-plugin-content.html index c306b73a7a6..06651d0f4ce 100644 --- a/src/core/siteplugins/components/plugin-content/core-siteplugins-plugin-content.html +++ b/src/core/siteplugins/components/plugin-content/core-siteplugins-plugin-content.html @@ -1,3 +1,3 @@ - + From 9dbe9150b805f19ac5bde0c3983fba7016383216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 28 Aug 2019 10:40:51 +0200 Subject: [PATCH 229/241] MOBILE-3068 scorm: Reduce index page length --- .../index/addon-mod-scorm-index.html | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html b/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html index 20ca8751fbb..5aeb8dec523 100644 --- a/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html +++ b/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html @@ -31,38 +31,38 @@

{{ 'addon.mod_scorm.attempts' | translate }}

-

{{ 'addon.mod_scorm.noattemptsallowed' | translate }}

-

{{ 'core.unlimited' | translate }}

-

{{ scorm.maxattempt }}

+

{{ 'addon.mod_scorm.noattemptsallowed' | translate }}

+

{{ 'core.unlimited' | translate }}

+

{{ scorm.maxattempt }}

-

{{ 'addon.mod_scorm.noattemptsmade' | translate }}

-

{{ scorm.numAttempts }}

+

{{ 'addon.mod_scorm.noattemptsmade' | translate }}

+

{{ scorm.numAttempts }}

-

{{ 'addon.mod_scorm.gradeforattempt' | translate }} {{attempt.number}}

-

{{ attempt.grade }}

-

{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}

+

{{ 'addon.mod_scorm.gradeforattempt' | translate }} {{attempt.number}}

+

{{ attempt.grade }}

+

{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}

-

{{ 'addon.mod_scorm.gradeforattempt' | translate }} {{attempt.number}}

-

{{ attempt.grade }}

-

{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}

-

{{ 'addon.mod_scorm.offlineattemptnote' | translate }}

-

{{ 'addon.mod_scorm.offlineattemptovermax' | translate }}

+

{{ 'addon.mod_scorm.gradeforattempt' | translate }} {{attempt.number}}

+

{{ attempt.grade }}

+

{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}

+

{{ 'addon.mod_scorm.offlineattemptnote' | translate }}

+

{{ 'addon.mod_scorm.offlineattemptovermax' | translate }}

-

{{ 'addon.mod_scorm.grademethod' | translate }}

-

{{ scorm.gradeMethodReadable }}

+

{{ 'addon.mod_scorm.grademethod' | translate }}

+

{{ scorm.gradeMethodReadable }}

-

{{ 'addon.mod_scorm.gradereported' | translate }}

-

{{ scorm.grade }}

-

{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}

+

{{ 'addon.mod_scorm.gradereported' | translate }}

+

{{ scorm.grade }}

+

{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}

-

{{ 'core.lastsync' | translate }}

+

{{ 'core.lastsync' | translate }}

{{ syncTime }}

@@ -130,7 +130,7 @@

{{ 'addon.mod_scorm.contents' | translate }}

-

{{ 'addon.mod_scorm.mode' | translate }}

+

{{ 'addon.mod_scorm.mode' | translate }}

{{ 'addon.mod_scorm.browse' | translate }} From 344a3c639f999b23886dfe33cbb933755d11738d Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 28 Aug 2019 12:40:43 +0200 Subject: [PATCH 230/241] MOBILE-3068 blocks: Don't use phantom tabs in title blocks --- .../components/only-title-block/only-title-block.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/core/block/components/only-title-block/only-title-block.ts b/src/core/block/components/only-title-block/only-title-block.ts index a128c44d130..2c1145003c5 100644 --- a/src/core/block/components/only-title-block/only-title-block.ts +++ b/src/core/block/components/only-title-block/only-title-block.ts @@ -13,8 +13,9 @@ // limitations under the License. import { Injector, OnInit, Component } from '@angular/core'; +import { NavController } from 'ionic-angular'; import { CoreBlockBaseComponent } from '../../classes/base-block-component'; -import { CoreLoginHelperProvider } from '@core/login/providers/helper'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; /** * Component to render blocks with only a title and link. @@ -25,11 +26,8 @@ import { CoreLoginHelperProvider } from '@core/login/providers/helper'; }) export class CoreBlockOnlyTitleComponent extends CoreBlockBaseComponent implements OnInit { - protected loginHelper: CoreLoginHelperProvider; - - constructor(injector: Injector) { + constructor(injector: Injector, protected navCtrl: NavController, protected linkHelper: CoreContentLinksHelperProvider) { super(injector, 'CoreBlockOnlyTitleComponent'); - this.loginHelper = injector.get(CoreLoginHelperProvider); } /** @@ -45,6 +43,6 @@ export class CoreBlockOnlyTitleComponent extends CoreBlockBaseComponent impleme * Go to the block page. */ gotoBlock(): void { - this.loginHelper.redirect(this.link, this.linkParams); + this.linkHelper.goInSite(this.navCtrl, this.link, this.linkParams, undefined, true); } } From c6c6c753f02fe1a2109941ea9c71cdfc9bfb20df Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 28 Aug 2019 16:02:45 +0200 Subject: [PATCH 231/241] MOBILE-3068 forum: Don't use phantom tab in forum index links --- src/addon/mod/forum/providers/index-link-handler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/addon/mod/forum/providers/index-link-handler.ts b/src/addon/mod/forum/providers/index-link-handler.ts index b975f449c7f..2b7b23e7c2b 100644 --- a/src/addon/mod/forum/providers/index-link-handler.ts +++ b/src/addon/mod/forum/providers/index-link-handler.ts @@ -68,7 +68,8 @@ export class AddonModForumIndexLinkHandler extends CoreContentLinksModuleIndexHa forumId = parseInt(params.f, 10); this.courseProvider.getModuleBasicInfoByInstance(forumId, 'forum', siteId).then((module) => { - this.courseHelper.navigateToModule(parseInt(module.id, 10), siteId, module.course); + this.courseHelper.navigateToModule(parseInt(module.id, 10), siteId, module.course, undefined, + undefined, undefined, navCtrl); }).finally(() => { // Just in case. In fact we need to dismiss the modal before showing a toast or error message. modal.dismiss(); From 26dd20afff42e419fd2267ed0fd4219367e971e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 29 Aug 2019 10:06:31 +0200 Subject: [PATCH 232/241] MOBILE-3068 feedback: Fix range error format --- src/addon/mod/feedback/pages/form/form.html | 2 +- src/app/app.scss | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/addon/mod/feedback/pages/form/form.html b/src/addon/mod/feedback/pages/form/form.html index 2725d3525a6..9bf4f4d1a0a 100644 --- a/src/addon/mod/feedback/pages/form/form.html +++ b/src/addon/mod/feedback/pages/form/form.html @@ -30,7 +30,7 @@

{{ 'addon.mod_feedback.mode' | translate }}

-

{{ 'addon.mod_feedback.numberoutofrange' | translate }} [{{item.rangefrom}}, {{item.rangeto}}]

+

{{ 'addon.mod_feedback.numberoutofrange' | translate }} [{{item.rangefrom}}, {{item.rangeto}}]

diff --git a/src/app/app.scss b/src/app/app.scss index 811d7724cd7..594dccfe368 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -718,15 +718,21 @@ ion-app.app-root { .core-#{$color-name}-item.item-input { border-bottom: 0 !important; - &.item-md .item-inner { + &.item-md .item-inner, + &.item-md.item-input.ng-valid.item-input-has-value:not(.input-has-focus):not(.item-input-has-focus) .item-inner, + &.item-md.item-input.ng-valid.input-has-value:not(.input-has-focus):not(.item-input-has-focus) .item-inner { @include md-input-highlight($color-base); } - &.item-ios .item-inner { + &.item-ios .item-inner, + &.item-ios.item-input.ng-valid.item-input-has-value:not(.input-has-focus):not(.item-input-has-focus) .item-inner, + &.item-ios.item-input.ng-valid.input-has-value:not(.input-has-focus):not(.item-input-has-focus) .item-inner { @include ios-input-highlight($color-base); } - &.item-wp .item-inner { + &.item-wp .text-input, + &.item-wp.item-input.ng-valid.item-input-has-value:not(.input-has-focus):not(.item-input-has-focus) .text-input, + &.item-wp.item-input.ng-valid.input-has-value:not(.input-has-focus):not(.item-input-has-focus) .text-input { border-color: $color-base; } } From 392031951162b7acfd988c0013dc2edf00024799 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 29 Aug 2019 10:37:01 +0200 Subject: [PATCH 233/241] MOBILE-3068 feedback: Fix offline warning shown when it shouldn't --- src/addon/mod/feedback/components/index/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/addon/mod/feedback/components/index/index.ts b/src/addon/mod/feedback/components/index/index.ts index 18aa04c6551..eefc27a7e92 100644 --- a/src/addon/mod/feedback/components/index/index.ts +++ b/src/addon/mod/feedback/components/index/index.ts @@ -195,14 +195,14 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity } return this.fetchFeedbackOverviewData(this.access); - }).then(() => { - // All data obtained, now fill the context menu. + }).finally(() => { + // Now fill the context menu. this.fillContextMenu(refresh); // Check if there are responses stored in offline. - return this.feedbackOffline.hasFeedbackOfflineData(this.feedback.id); - }).then((hasOffline) => { - this.hasOffline = hasOffline; + return this.feedbackOffline.hasFeedbackOfflineData(this.feedback.id).then((hasOffline) => { + this.hasOffline = hasOffline; + }); }); } From 1700f8947d53006286d76fe40a4a954e4f339f8b Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 29 Aug 2019 11:20:43 +0200 Subject: [PATCH 234/241] MOBILE-3068 calendar: Calculate istoday in the app --- .../calendar/components/calendar/addon-calendar-calendar.html | 2 +- src/addon/calendar/components/calendar/calendar.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index 03037725123..093b97064f5 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -39,7 +39,7 @@

{{ periodName }}

- +

{{ day.mday }}

diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 985f284c8e2..7b088b76f6d 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -205,9 +205,12 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.calculateIsCurrentMonth(); if (this.isCurrentMonth) { + const currentDay = new Date().getDate(); let isPast = true; + this.weeks.forEach((week) => { week.days.some((day) => { + day.istoday = day.mday == currentDay; day.ispast = isPast && !day.istoday; isPast = day.ispast; From af461e7dacf7d1807c0782581381002f7189dbb1 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 29 Aug 2019 13:42:04 +0200 Subject: [PATCH 235/241] MOBILE-3068 calendar: Autofetch day&month when event changed --- .../calendar/components/calendar/calendar.ts | 10 ++- .../upcoming-events/upcoming-events.ts | 10 ++- src/addon/calendar/pages/day/day.ts | 20 +++-- .../calendar/pages/edit-event/edit-event.ts | 2 +- src/addon/calendar/pages/event/event.ts | 4 +- src/addon/calendar/pages/index/index.ts | 19 ++--- src/addon/calendar/providers/calendar-sync.ts | 2 +- src/addon/calendar/providers/calendar.ts | 37 ++++++--- src/addon/calendar/providers/helper.ts | 80 ++++++++++++------- 9 files changed, 119 insertions(+), 65 deletions(-) diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 7b088b76f6d..269310608ef 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -285,14 +285,16 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest /** * Refresh events. * - * @param {boolean} [sync] Whether it should try to synchronize offline events. - * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @param {boolean} [afterChange] Whether the refresh is done after an event has changed or has been synced. * @return {Promise} Promise resolved when done. */ - refreshData(sync?: boolean, showErrors?: boolean): Promise { + refreshData(afterChange?: boolean): Promise { const promises = []; - promises.push(this.calendarProvider.invalidateMonthlyEvents(this.year, this.month)); + // Don't invalidate monthly events after a change, it has already been handled. + if (!afterChange) { + promises.push(this.calendarProvider.invalidateMonthlyEvents(this.year, this.month)); + } promises.push(this.coursesProvider.invalidateCategories(0, true)); promises.push(this.calendarProvider.invalidateTimeFormat()); diff --git a/src/addon/calendar/components/upcoming-events/upcoming-events.ts b/src/addon/calendar/components/upcoming-events/upcoming-events.ts index 74db1aebcdc..d56a3470bd4 100644 --- a/src/addon/calendar/components/upcoming-events/upcoming-events.ts +++ b/src/addon/calendar/components/upcoming-events/upcoming-events.ts @@ -225,14 +225,16 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, /** * Refresh events. * - * @param {boolean} [sync] Whether it should try to synchronize offline events. - * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @param {boolean} [afterChange] Whether the refresh is done after an event has changed or has been synced. * @return {Promise} Promise resolved when done. */ - refreshData(sync?: boolean, showErrors?: boolean): Promise { + refreshData(afterChange?: boolean): Promise { const promises = []; - promises.push(this.calendarProvider.invalidateAllUpcomingEvents()); + // Don't invalidate upcoming events after a change, it has already been handled. + if (!afterChange) { + promises.push(this.calendarProvider.invalidateAllUpcomingEvents()); + } promises.push(this.coursesProvider.invalidateCategories(0, true)); promises.push(this.calendarProvider.invalidateLookAhead()); promises.push(this.calendarProvider.invalidateTimeFormat()); diff --git a/src/addon/calendar/pages/day/day.ts b/src/addon/calendar/pages/day/day.ts index cc930e7285f..c3b566f889f 100644 --- a/src/addon/calendar/pages/day/day.ts +++ b/src/addon/calendar/pages/day/day.ts @@ -113,35 +113,35 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { if (data && data.event) { this.loaded = false; - this.refreshData(true, false); + this.refreshData(true, false, true); } }, this.currentSiteId); // Listen for new event discarded event. When it does, reload the data. this.discardedObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => { this.loaded = false; - this.refreshData(true, false); + this.refreshData(true, false, true); }, this.currentSiteId); // Listen for events edited. When an event is edited, reload the data. this.editEventObserver = eventsProvider.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => { if (data && data.event) { this.loaded = false; - this.refreshData(true, false); + this.refreshData(true, false, true); } }, this.currentSiteId); // Refresh data if calendar events are synchronized automatically. this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => { this.loaded = false; - this.refreshData(); + this.refreshData(false, false, true); }, this.currentSiteId); // Refresh data if calendar events are synchronized manually but not by this page. this.manualSyncObserver = eventsProvider.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => { if (data && (data.source != 'day' || data.year != this.year || data.month != this.month || data.day != this.day)) { this.loaded = false; - this.refreshData(); + this.refreshData(false, false, true); } }, this.currentSiteId); @@ -153,7 +153,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.deletedEvents.push(data.eventId); } else { this.loaded = false; - this.refreshData(); + this.refreshData(false, false, true); } }, this.currentSiteId); @@ -425,15 +425,19 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { * * @param {boolean} [sync] Whether it should try to synchronize offline events. * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @param {boolean} [afterChange] Whether the refresh is done after an event has changed or has been synced. * @return {Promise} Promise resolved when done. */ - refreshData(sync?: boolean, showErrors?: boolean): Promise { + refreshData(sync?: boolean, showErrors?: boolean, afterChange?: boolean): Promise { this.syncIcon = 'spinner'; const promises = []; + // Don't invalidate day events after a change, it has already been handled. + if (!afterChange) { + promises.push(this.calendarProvider.invalidateDayEvents(this.year, this.month, this.day)); + } promises.push(this.calendarProvider.invalidateAllowedEventTypes()); - promises.push(this.calendarProvider.invalidateDayEvents(this.year, this.month, this.day)); promises.push(this.coursesProvider.invalidateCategories(0, true)); promises.push(this.calendarProvider.invalidateTimeFormat()); diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index 4cc40a19431..b8d8ef9f409 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -499,7 +499,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { const numberOfRepetitions = formData.repeat ? formData.repeats : (data.repeateditall && this.event.othereventscount ? this.event.othereventscount + 1 : 1); - this.calendarHelper.invalidateRepeatedEventsOnCalendarForEvent(result.event, numberOfRepetitions).catch(() => { + return this.calendarHelper.refreshAfterChangeEvent(result.event, numberOfRepetitions).catch(() => { // Ignore errors. }); } diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index d23579df07f..8edc9b96eac 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -449,8 +449,8 @@ export class AddonCalendarEventPage implements OnDestroy { if (sent) { // Event deleted, invalidate right days & months. - promise = this.calendarHelper.invalidateRepeatedEventsOnCalendarForEvent(this.event, - deleteAll ? this.event.eventcount : 1).catch(() => { + promise = this.calendarHelper.refreshAfterChangeEvent(this.event, deleteAll ? this.event.eventcount : 1) + .catch(() => { // Ignore errors. }); } else { diff --git a/src/addon/calendar/pages/index/index.ts b/src/addon/calendar/pages/index/index.ts index 341cf36b3bb..8f3d5a129da 100644 --- a/src/addon/calendar/pages/index/index.ts +++ b/src/addon/calendar/pages/index/index.ts @@ -95,42 +95,42 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { if (data && data.event) { this.loaded = false; - this.refreshData(true, false); + this.refreshData(true, false, true); } }, this.currentSiteId); // Listen for new event discarded event. When it does, reload the data. this.discardedObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => { this.loaded = false; - this.refreshData(true, false); + this.refreshData(true, false, true); }, this.currentSiteId); // Listen for events edited. When an event is edited, reload the data. this.editEventObserver = eventsProvider.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => { if (data && data.event) { this.loaded = false; - this.refreshData(true, false); + this.refreshData(true, false, true); } }, this.currentSiteId); // Refresh data if calendar events are synchronized automatically. this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => { this.loaded = false; - this.refreshData(); + this.refreshData(false, false, true); }, this.currentSiteId); // Refresh data if calendar events are synchronized manually but not by this page. this.manualSyncObserver = eventsProvider.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => { if (data && data.source != 'index') { this.loaded = false; - this.refreshData(); + this.refreshData(false, false, true); } }, this.currentSiteId); // Update the events when an event is deleted. this.deleteEventObserver = eventsProvider.on(AddonCalendarProvider.DELETED_EVENT_EVENT, (data) => { this.loaded = false; - this.refreshData(); + this.refreshData(false, false, true); }, this.currentSiteId); // Update the "hasOffline" property if an event deleted in offline is restored. @@ -251,9 +251,10 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { * * @param {boolean} [sync] Whether it should try to synchronize offline events. * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @param {boolean} [afterChange] Whether the refresh is done after an event has changed or has been synced. * @return {Promise} Promise resolved when done. */ - refreshData(sync?: boolean, showErrors?: boolean): Promise { + refreshData(sync?: boolean, showErrors?: boolean, afterChange?: boolean): Promise { this.syncIcon = 'spinner'; const promises = []; @@ -262,9 +263,9 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { // Refresh the sub-component. if (this.showCalendar && this.calendarComponent) { - promises.push(this.calendarComponent.refreshData()); + promises.push(this.calendarComponent.refreshData(afterChange)); } else if (!this.showCalendar && this.upcomingEventsComponent) { - promises.push(this.upcomingEventsComponent.refreshData()); + promises.push(this.upcomingEventsComponent.refreshData(afterChange)); } return Promise.all(promises).finally(() => { diff --git a/src/addon/calendar/providers/calendar-sync.ts b/src/addon/calendar/providers/calendar-sync.ts index b395ae6af19..bc9d96f3373 100644 --- a/src/addon/calendar/providers/calendar-sync.ts +++ b/src/addon/calendar/providers/calendar-sync.ts @@ -159,7 +159,7 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { // Data has been sent to server. Now invalidate the WS calls. const promises = [ this.calendarProvider.invalidateEventsList(siteId), - this.calendarHelper.invalidateRepeatedEventsOnCalendar(result.toinvalidate, siteId) + this.calendarHelper.refreshAfterChangeEvents(result.toinvalidate, siteId) ]; return Promise.all(promises).catch(() => { diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 17f372590d4..892cb35cd91 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; -import { CoreSite } from '@classes/site'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreAppProvider } from '@providers/app'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -971,10 +971,12 @@ export class AddonCalendarProvider { * @param {number} day Day to get. * @param {number} [courseId] Course to get. * @param {number} [categoryId] Category to get. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the response. */ - getDayEvents(year: number, month: number, day: number, courseId?: number, categoryId?: number, siteId?: string): Promise { + getDayEvents(year: number, month: number, day: number, courseId?: number, categoryId?: number, ignoreCache?: boolean, + siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { @@ -991,11 +993,16 @@ export class AddonCalendarProvider { data.categoryid = categoryId; } - const preSets = { + const preSets: CoreSiteWSPreSets = { cacheKey: this.getDayEventsCacheKey(year, month, day, courseId, categoryId), updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('core_calendar_get_calendar_day_view', data, preSets).then((response) => { this.storeEventsInLocalDB(response.events, siteId); @@ -1159,7 +1166,6 @@ export class AddonCalendarProvider { return site.getDb().getRecords(AddonCalendarProvider.EVENTS_TABLE, {repeatid: repeatId}); }); } - /** * Get monthly calendar events. * @@ -1167,10 +1173,12 @@ export class AddonCalendarProvider { * @param {number} month Month to get. * @param {number} [courseId] Course to get. * @param {number} [categoryId] Category to get. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the response. */ - getMonthlyEvents(year: number, month: number, courseId?: number, categoryId?: number, siteId?: string): Promise { + getMonthlyEvents(year: number, month: number, courseId?: number, categoryId?: number, ignoreCache?: boolean, siteId?: string) + : Promise { return this.sitesProvider.getSite(siteId).then((site) => { @@ -1180,7 +1188,7 @@ export class AddonCalendarProvider { }; // This parameter requires Moodle 3.5. - if ( site.isVersionGreaterEqualThan('3.5')) { + if (site.isVersionGreaterEqualThan('3.5')) { // Set mini to 1 to prevent returning the course selector HTML. data.mini = 1; } @@ -1192,11 +1200,16 @@ export class AddonCalendarProvider { data.categoryid = categoryId; } - const preSets = { + const preSets: CoreSiteWSPreSets = { cacheKey: this.getMonthlyEventsCacheKey(year, month, courseId, categoryId), updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('core_calendar_get_calendar_monthly_view', data, preSets).then((response) => { response.weeks.forEach((week) => { week.days.forEach((day) => { @@ -1253,10 +1266,11 @@ export class AddonCalendarProvider { * * @param {number} [courseId] Course to get. * @param {number} [categoryId] Category to get. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the response. */ - getUpcomingEvents(courseId?: number, categoryId?: number, siteId?: string): Promise { + getUpcomingEvents(courseId?: number, categoryId?: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { @@ -1269,11 +1283,16 @@ export class AddonCalendarProvider { data.categoryid = categoryId; } - const preSets = { + const preSets: CoreSiteWSPreSets = { cacheKey: this.getUpcomingEventsCacheKey(courseId, categoryId), updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('core_calendar_get_calendar_upcoming_view', data, preSets).then((response) => { this.storeEventsInLocalDB(response.events, siteId); diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index 36d1f651856..7fd2226599b 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -342,29 +342,36 @@ export class AddonCalendarHelperProvider { } /** - * Invalidate all calls from calendar WS calls. + * Refresh the month & day for several created/edited/deleted events, and invalidate the months & days + * for their repeated events if needed. * * @param {{event: any, repeated: number}[]} events Events that have been touched and number of times each event is repeated. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Resolved when done. */ - invalidateRepeatedEventsOnCalendar(events: {event: any, repeated: number}[], siteId?: string): Promise { + refreshAfterChangeEvents(events: {event: any, repeated: number}[], siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - const timestarts = []; + const fetchTimestarts = [], + invalidateTimestarts = []; + + // Always fetch upcoming events. + const upcomingPromise = this.calendarProvider.getUpcomingEvents(undefined, undefined, true, site.id).catch(() => { + // Ignore errors. + }); // Invalidate the events and get the timestarts so we can invalidate months & days. - return this.utils.allPromises(events.map((eventData) => { + return this.utils.allPromises([upcomingPromise].concat(events.map((eventData) => { if (eventData.repeated > 1) { if (eventData.event.repeatid) { // Being edited or deleted. // We need to calculate the days to invalidate because the event date could have changed. // We don't know if the repeated events are before or after this one, invalidate them all. - timestarts.push(eventData.event.timestart); + fetchTimestarts.push(eventData.event.timestart); for (let i = 1; i < eventData.repeated; i++) { - timestarts.push(eventData.event.timestart + CoreConstants.SECONDS_DAY * 7 * i); - timestarts.push(eventData.event.timestart - CoreConstants.SECONDS_DAY * 7 * i); + invalidateTimestarts.push(eventData.event.timestart + CoreConstants.SECONDS_DAY * 7 * i); + invalidateTimestarts.push(eventData.event.timestart - CoreConstants.SECONDS_DAY * 7 * i); } // Get the repeated events to invalidate them. @@ -378,48 +385,66 @@ export class AddonCalendarHelperProvider { } else { // Being added. let time = eventData.event.timestart; - while (eventData.repeated > 0) { - timestarts.push(time); + fetchTimestarts.push(time); + + while (eventData.repeated > 1) { time += CoreConstants.SECONDS_DAY * 7; eventData.repeated--; + invalidateTimestarts.push(time); } return Promise.resolve(); } } else { // Not repeated. - timestarts.push(eventData.event.timestart); + fetchTimestarts.push(eventData.event.timestart); return this.calendarProvider.invalidateEvent(eventData.event.id); } - })).finally(() => { - const invalidatedMonths = {}, - invalidatedDays = {}; + }))).finally(() => { + const treatedMonths = {}, + treatedDays = {}; return this.utils.allPromises([ this.calendarProvider.invalidateAllUpcomingEvents(), - // Invalidate months and days. - this.utils.allPromises(timestarts.map((time) => { + // Fetch or invalidate months and days. + this.utils.allPromises(fetchTimestarts.concat(invalidateTimestarts).map((time, index) => { const promises = [], day = moment(new Date(time * 1000)), monthId = this.getMonthId(day.year(), day.month() + 1), dayId = monthId + '#' + day.date(); - if (!invalidatedMonths[monthId]) { - // Month not invalidated already, do it now. - invalidatedMonths[monthId] = monthId; + if (!treatedMonths[monthId]) { + // Month not treated already, do it now. + treatedMonths[monthId] = monthId; - promises.push(this.calendarProvider.invalidateMonthlyEvents(day.year(), day.month() + 1, site.id)); + if (index < fetchTimestarts.length) { + promises.push(this.calendarProvider.getMonthlyEvents(day.year(), day.month() + 1, undefined, + undefined, true, site.id).catch(() => { + + // Ignore errors. + })); + } else { + promises.push(this.calendarProvider.invalidateMonthlyEvents(day.year(), day.month() + 1, site.id)); + } } - if (!invalidatedDays[dayId]) { + if (!treatedDays[dayId]) { // Day not invalidated already, do it now. - invalidatedDays[dayId] = dayId; - - promises.push(this.calendarProvider.invalidateDayEvents(day.year(), day.month() + 1, day.date(), - site.id)); + treatedDays[dayId] = dayId; + + if (index < fetchTimestarts.length) { + promises.push(this.calendarProvider.getDayEvents(day.year(), day.month() + 1, day.date(), + undefined, undefined, true, site.id).catch(() => { + + // Ignore errors. + })); + } else { + promises.push(this.calendarProvider.invalidateDayEvents(day.year(), day.month() + 1, day.date(), + site.id)); + } } return this.utils.allPromises(promises); @@ -430,14 +455,15 @@ export class AddonCalendarHelperProvider { } /** - * Invalidate all calls from calendar WS calls. + * Refresh the month & day for a created/edited/deleted event, and invalidate the months & days + * for their repeated events if needed. * * @param {any} event Event that has been touched. * @param {number} repeated Number of times the event is repeated. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Resolved when done. */ - invalidateRepeatedEventsOnCalendarForEvent(event: any, repeated: number, siteId?: string): Promise { - return this.invalidateRepeatedEventsOnCalendar([{event: event, repeated: repeated}], siteId); + refreshAfterChangeEvent(event: any, repeated: number, siteId?: string): Promise { + return this.refreshAfterChangeEvents([{event: event, repeated: repeated}], siteId); } } From 1b94724575d6215ec9f0b3f6ee7fb078aeaf1012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 29 Aug 2019 17:56:23 +0200 Subject: [PATCH 236/241] MOBILE-3068 calendar: Add colors to event types to match monthly --- .../components/calendar/calendar.scss | 39 +++++++++++++++++++ .../addon-calendar-upcoming-events.html | 2 +- src/addon/calendar/pages/day/day.html | 2 +- src/addon/calendar/pages/list/list.html | 2 +- 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/addon/calendar/components/calendar/calendar.scss b/src/addon/calendar/components/calendar/calendar.scss index ff2d77c97aa..6558abcb438 100644 --- a/src/addon/calendar/components/calendar/calendar.scss +++ b/src/addon/calendar/components/calendar/calendar.scss @@ -8,6 +8,45 @@ $calendar-today-bgcolor: $core-color !default; $calendar-today-color: $white !default; $calendar-border-color: $gray !default; +ion-app.app-root page-addon-calendar-list, +ion-app.app-root page-addon-calendar-day, +ion-app.app-root addon-calendar-upcoming-events { + + .item.addon-calendar-event .icon { + color: white; + border-radius: 50%; + width: 36px; + height: 36px; + line-height: 36px; + + &.fa { + font-size: 20px; + &::before { + width: 1.9em; + } + } + } + .item.addon-calendar-event .core-module-icon { + margin: 9px 8px 9px 8px; + } + + .item.addon-calendar-eventtype-category .icon { + background-color: $calendar-event-category-color; + } + .item.addon-calendar-eventtype-course .icon { + background-color: $calendar-event-course-color; + } + .item.addon-calendar-eventtype-group .icon { + background-color: $calendar-event-group-color; + } + .item.addon-calendar-eventtype-user .icon { + background-color: $calendar-event-user-color; + } + .item.addon-calendar-eventtype-site .icon { + background-color: $calendar-event-site-color; + } +} + ion-app.app-root addon-calendar-calendar { .addon-calendar-navigation { diff --git a/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html b/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html index 0a4b7bb1ecc..68f1746083b 100644 --- a/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html +++ b/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html @@ -4,7 +4,7 @@ -
+

diff --git a/src/addon/calendar/pages/day/day.html b/src/addon/calendar/pages/day/day.html index 9cfd3705a0a..34335a7dee5 100644 --- a/src/addon/calendar/pages/day/day.html +++ b/src/addon/calendar/pages/day/day.html @@ -48,7 +48,7 @@

{{ periodName }}

- +

diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index 2d85b3f1d5a..3ae4f4c0d52 100644 --- a/src/addon/calendar/pages/list/list.html +++ b/src/addon/calendar/pages/list/list.html @@ -32,7 +32,7 @@ {{ event.timestart * 1000 | coreFormatDate: "strftimedayshort" }} -
+

From f5426eba9ed7714c11f848de5f947567e66ecd91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 30 Aug 2019 08:35:16 +0200 Subject: [PATCH 237/241] MOBILE-3068 calendar: Fix delete restore icons --- .../components/calendar/calendar.scss | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/src/addon/calendar/components/calendar/calendar.scss b/src/addon/calendar/components/calendar/calendar.scss index 6558abcb438..b04fe3d64c5 100644 --- a/src/addon/calendar/components/calendar/calendar.scss +++ b/src/addon/calendar/components/calendar/calendar.scss @@ -12,38 +12,40 @@ ion-app.app-root page-addon-calendar-list, ion-app.app-root page-addon-calendar-day, ion-app.app-root addon-calendar-upcoming-events { - .item.addon-calendar-event .icon { - color: white; - border-radius: 50%; - width: 36px; - height: 36px; - line-height: 36px; - - &.fa { - font-size: 20px; - &::before { - width: 1.9em; + .item.addon-calendar-event { + > .icon { + color: white; + border-radius: 50%; + width: 36px; + height: 36px; + line-height: 36px; + + &.fa { + font-size: 20px; + &::before { + width: 1.9em; + } } } - } - .item.addon-calendar-event .core-module-icon { - margin: 9px 8px 9px 8px; - } + > .core-module-icon { + margin: 9px 8px 9px 8px; + } - .item.addon-calendar-eventtype-category .icon { - background-color: $calendar-event-category-color; - } - .item.addon-calendar-eventtype-course .icon { - background-color: $calendar-event-course-color; - } - .item.addon-calendar-eventtype-group .icon { - background-color: $calendar-event-group-color; - } - .item.addon-calendar-eventtype-user .icon { - background-color: $calendar-event-user-color; - } - .item.addon-calendar-eventtype-site .icon { - background-color: $calendar-event-site-color; + &.addon-calendar-eventtype-category > .icon { + background-color: $calendar-event-category-color; + } + &.addon-calendar-eventtype-course > .icon { + background-color: $calendar-event-course-color; + } + &.addon-calendar-eventtype-group > .icon { + background-color: $calendar-event-group-color; + } + &.addon-calendar-eventtype-user > .icon { + background-color: $calendar-event-user-color; + } + &.addon-calendar-eventtype-site > .icon { + background-color: $calendar-event-site-color; + } } } From 47a803dca7661bfc930f8dee7659483af44bb9f8 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 30 Aug 2019 09:55:06 +0200 Subject: [PATCH 238/241] MOBILE-3068 core: Lock plugin and libraries versions --- config.xml | 42 ++++---- package-lock.json | 248 ++++++++++++++++------------------------------ package.json | 170 +++++++++++++++---------------- 3 files changed, 193 insertions(+), 267 deletions(-) diff --git a/config.xml b/config.xml index 744ea653e08..1ab1ad371ee 100644 --- a/config.xml +++ b/config.xml @@ -113,33 +113,33 @@ - - + + - - - - + + + + - - + + - - - - + + + + - - - - - - - - - + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index d08a2f82fa3..c82b49deb62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -498,8 +498,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -520,14 +519,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -542,20 +539,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -672,8 +666,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -685,7 +678,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -700,7 +692,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -708,14 +699,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -734,7 +723,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -815,8 +803,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -828,7 +815,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -914,8 +900,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -951,7 +936,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -971,7 +955,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -1015,14 +998,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -3220,7 +3201,8 @@ }, "cached-path-relative": { "version": "1.0.1", - "resolved": "" + "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.1.tgz", + "integrity": "sha1-0JxLUoAKpMB44t2BqGmqyQ0uVOc=" }, "caller-path": { "version": "0.1.0", @@ -4528,7 +4510,8 @@ }, "fstream": { "version": "1.0.11", - "resolved": "", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", "requires": { "graceful-fs": "^4.1.2", "inherits": "~2.0.0", @@ -7488,9 +7471,9 @@ } }, "cordova-android-support-gradle-release": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cordova-android-support-gradle-release/-/cordova-android-support-gradle-release-3.0.0.tgz", - "integrity": "sha512-vyiqQ6N9Qb+4xRizWSpUX/LyJ1HaDN0piWc8xoS9Hx9YodIS3vyi1UpQyfLQmCixoeLVcRieKXjuSMXnUrv1dw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cordova-android-support-gradle-release/-/cordova-android-support-gradle-release-3.0.1.tgz", + "integrity": "sha512-RSW55DkSckmqhX/kjj+a1YeVdy7s/AtlZn6Qa5XMQmmA4Iogq+IF2jvInZqzCF19DbI5YE95AP7VDbRk+DdDRw==", "requires": { "q": "^1.4.1", "semver": "5.6.0" @@ -7504,9 +7487,9 @@ } }, "cordova-clipboard": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/cordova-clipboard/-/cordova-clipboard-1.2.1.tgz", - "integrity": "sha512-WTGxyQJYsgmll8wDEo0u4XevZDUH1ZH1VPoOwwNkQ8YOtCNQS8gRIIVtZ70Kan+Vo+CiUMV0oJXdNAdARb8JwQ==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/cordova-clipboard/-/cordova-clipboard-1.3.0.tgz", + "integrity": "sha512-IGk4LZm/DJ0Xk/jgakHm4wa+A/lrRP3QfzMAHDG7oWLJS4ISOpfI32Wez4ndnENItRslGyBVyJyKD83CxELCAw==" }, "cordova-ios": { "version": "4.5.5", @@ -7808,24 +7791,24 @@ "integrity": "sha512-RhIBtd5xhD/iLnxjt35jvOae28oNW/wtMZBOmQR3Rf0y4wirvA1bpAZEhBoFqL+rZGhsd6ddOdQXdex1T0DRyQ==" }, "cordova-plugin-camera": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/cordova-plugin-camera/-/cordova-plugin-camera-4.0.3.tgz", - "integrity": "sha1-c3Olk4MYyGzP2E43E+I4LRD+B2s=" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cordova-plugin-camera/-/cordova-plugin-camera-4.1.0.tgz", + "integrity": "sha512-fCLhWjWYn49q3X5xaypAPgTz6MAWSKFFQvD2Gpi5SuVlrRPRphtX2jIqR2zCBuDTBR082QVnlc+yUDXt65Mjgw==" }, "cordova-plugin-customurlscheme": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/cordova-plugin-customurlscheme/-/cordova-plugin-customurlscheme-4.3.0.tgz", - "integrity": "sha1-Avlod4tAk5kOsEB/P6GxRY1wX5Q=" + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/cordova-plugin-customurlscheme/-/cordova-plugin-customurlscheme-4.4.0.tgz", + "integrity": "sha512-7VPJnNfvfZQSU1IdhJX7BpDgvC7bEe+Kfg9Cj8guSoZDcTi378qQFb6VOwthT8hwGXx2bZzWf0qnTZdRlLQy+Q==" }, "cordova-plugin-device": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/cordova-plugin-device/-/cordova-plugin-device-2.0.2.tgz", - "integrity": "sha1-/Ajzci5n7ve2xnv8mag99q3Quro=" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/cordova-plugin-device/-/cordova-plugin-device-2.0.3.tgz", + "integrity": "sha512-Jb3V72btxf3XHpkPQsGdyc8N6tVBYn1vsxSFj43fIz9vonJDUThYPCJJHqk6PX6N4dJw6I4FjxkpfCR4LDYMlw==" }, "cordova-plugin-file": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/cordova-plugin-file/-/cordova-plugin-file-6.0.1.tgz", - "integrity": "sha1-SWBrjBWlaI1HKPkuSnMloGHeB/U=" + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/cordova-plugin-file/-/cordova-plugin-file-6.0.2.tgz", + "integrity": "sha512-m7cughw327CjONN/qjzsTpSesLaeybksQh420/gRuSXJX5Zt9NfgsSbqqKDon6jnQ9Mm7h7imgyO2uJ34XMBtA==" }, "cordova-plugin-file-opener2": { "version": "2.0.19", @@ -7843,9 +7826,9 @@ "integrity": "sha1-6sMVgQAphJOvowvolA5pj2HvvP4=" }, "cordova-plugin-inappbrowser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cordova-plugin-inappbrowser/-/cordova-plugin-inappbrowser-3.0.0.tgz", - "integrity": "sha1-1K4A02Z2IQdRBXrSWK5K1KkWGto=" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cordova-plugin-inappbrowser/-/cordova-plugin-inappbrowser-3.1.0.tgz", + "integrity": "sha512-YqrZfYgbGTS20SFID0mrRxud312VH072QVlFonCAkPgtGg1Svy7lELOCNFd+KU7t4mVtZeTEjZPEeefvjaetwQ==" }, "cordova-plugin-ionic-keyboard": { "version": "2.1.3", @@ -7857,34 +7840,34 @@ "from": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle" }, "cordova-plugin-media-capture": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/cordova-plugin-media-capture/-/cordova-plugin-media-capture-3.0.2.tgz", - "integrity": "sha1-2mV8L6rc/H/cKGjlnSFe2D5wDDo=" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/cordova-plugin-media-capture/-/cordova-plugin-media-capture-3.0.3.tgz", + "integrity": "sha512-pVQOrNM7VAuVUMXibAlMGIArrftHPrRs4dUCoE+e2HEFUp3LmN3Yj539LjdUxcWmz/A/cHC65m9E3DS56YJhcg==" }, "cordova-plugin-network-information": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/cordova-plugin-network-information/-/cordova-plugin-network-information-2.0.1.tgz", - "integrity": "sha1-6QQh9DDGq3bUCSI/Jfzvu7zhdpA=" + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/cordova-plugin-network-information/-/cordova-plugin-network-information-2.0.2.tgz", + "integrity": "sha512-NwO3qDBNL/vJxUxBTPNOA1HvkDf9eTeGH8JSZiwy1jq2W2mJKQEDBwqWkaEQS19Yd/MQTiw0cykxg5D7u4J6cQ==" }, "cordova-plugin-screen-orientation": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/cordova-plugin-screen-orientation/-/cordova-plugin-screen-orientation-3.0.1.tgz", - "integrity": "sha1-daNXzik4CB6PYdRgU5S213Rjwfg=" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/cordova-plugin-screen-orientation/-/cordova-plugin-screen-orientation-3.0.2.tgz", + "integrity": "sha512-2w6CMC+HGvbhogJetalwGurL2Fx8DQCCPy3wlSZHN1/W7WoQ5n9ujVozcoKrY4VaagK6bxrPFih+ElkO8Uqfzg==" }, "cordova-plugin-splashscreen": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/cordova-plugin-splashscreen/-/cordova-plugin-splashscreen-5.0.2.tgz", - "integrity": "sha1-dH509W4gHNWFvGLRS8oZ9oZ/8e0=" + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/cordova-plugin-splashscreen/-/cordova-plugin-splashscreen-5.0.3.tgz", + "integrity": "sha512-rnoDXMDfzoeHDBvsnu6JmzDE/pV5YJCAfc5hYX/Mb2BIXGgSjFJheByt0tU6kp3Wl40tSyFX4pYfBwFblBGyRg==" }, "cordova-plugin-statusbar": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/cordova-plugin-statusbar/-/cordova-plugin-statusbar-2.4.2.tgz", - "integrity": "sha1-/B+9wNjXAzp+jh8ff/FnrJvU+vY=" + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/cordova-plugin-statusbar/-/cordova-plugin-statusbar-2.4.3.tgz", + "integrity": "sha512-ThmXzl6QIKWFXf4wWw7Q/zpB+VKkz3VM958+5A0sXD4jmR++u7KnGttLksXshVwWr6lvGwUebLYtIyXwS4Ovcg==" }, "cordova-plugin-whitelist": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/cordova-plugin-whitelist/-/cordova-plugin-whitelist-1.3.3.tgz", - "integrity": "sha1-tehezbv+Wu3tQKG/TuI3LmfZb7Q=" + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/cordova-plugin-whitelist/-/cordova-plugin-whitelist-1.3.4.tgz", + "integrity": "sha512-EYC5eQFVkoYXq39l7tYKE6lEjHJ04mvTmKXxGL7quHLdFPfJMNzru/UYpn92AOfpl3PQaZmou78C7EgmFOwFQQ==" }, "cordova-plugin-zip": { "version": "3.1.0", @@ -9486,8 +9469,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -9505,13 +9487,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9524,18 +9504,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -9638,8 +9615,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -9649,7 +9625,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -9662,20 +9637,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.3.5", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -9692,7 +9664,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -9771,8 +9742,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -9782,7 +9752,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -9858,8 +9827,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -9889,7 +9857,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -9907,7 +9874,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -9946,13 +9912,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.0.3", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -10379,8 +10343,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -10401,14 +10364,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -10423,20 +10384,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -10553,8 +10511,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -10566,7 +10523,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -10581,7 +10537,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -10589,14 +10544,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -10615,7 +10568,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -10696,8 +10648,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -10709,7 +10660,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -10795,8 +10745,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -10832,7 +10781,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -10852,7 +10800,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -10896,14 +10843,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -16494,8 +16439,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -16516,14 +16460,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -16538,20 +16480,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -16668,8 +16607,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -16681,7 +16619,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -16696,7 +16633,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -16704,14 +16640,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -16730,7 +16664,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -16811,8 +16744,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -16824,7 +16756,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -16910,8 +16841,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -16947,7 +16877,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -16967,7 +16896,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -17011,14 +16939,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, diff --git a/package.json b/package.json index dc9a19748a2..8d5fda2e1ac 100644 --- a/package.json +++ b/package.json @@ -40,102 +40,102 @@ "windows.store": "npx electron-windows-store --input-directory .\\desktop\\dist\\win-unpacked --output-directory .\\desktop\\store -a .\\resources\\desktop -m .\\desktop\\assets\\windows\\AppXManifest.xml --package-version 0.0.0.0 --package-name MoodleDesktop" }, "dependencies": { - "@angular/animations": "^5.2.10", - "@angular/common": "^5.2.10", - "@angular/compiler": "^5.2.10", - "@angular/compiler-cli": "^5.2.10", - "@angular/core": "^5.2.10", - "@angular/forms": "^5.2.10", - "@angular/http": "^5.2.10", - "@angular/platform-browser": "^5.2.10", - "@angular/platform-browser-dynamic": "^5.2.10", - "@ionic-native/badge": "^4.17.0", - "@ionic-native/camera": "^4.17.0", - "@ionic-native/clipboard": "^4.17.0", - "@ionic-native/core": "^4.11.0", - "@ionic-native/device": "^4.17.0", - "@ionic-native/file": "^4.17.0", - "@ionic-native/file-opener": "^4.17.0", - "@ionic-native/file-transfer": "^4.17.0", - "@ionic-native/globalization": "^4.17.0", - "@ionic-native/in-app-browser": "^4.17.0", - "@ionic-native/keyboard": "^4.17.0", - "@ionic-native/local-notifications": "^4.17.0", - "@ionic-native/media-capture": "^4.17.0", - "@ionic-native/network": "^4.17.0", - "@ionic-native/push": "^4.17.0", - "@ionic-native/screen-orientation": "^4.17.0", - "@ionic-native/splash-screen": "^4.17.0", - "@ionic-native/sqlite": "^4.17.0", - "@ionic-native/status-bar": "^4.17.0", - "@ionic-native/web-intent": "^4.17.0", - "@ionic-native/zip": "^4.17.0", - "@ngx-translate/core": "^8.0.0", - "@ngx-translate/http-loader": "^2.0.1", - "@types/cordova": "^0.0.34", - "@types/cordova-plugin-file-transfer": "^0.0.3", - "@types/cordova-plugin-globalization": "^0.0.3", - "@types/cordova-plugin-network-information": "^0.0.3", - "@types/node": "^8.10.19", - "@types/promise.prototype.finally": "^2.0.2", - "chart.js": "^2.7.2", - "com-darryncampbell-cordova-plugin-intent": "^1.1.7", + "@angular/animations": "5.2.10", + "@angular/common": "5.2.10", + "@angular/compiler": "5.2.10", + "@angular/compiler-cli": "5.2.10", + "@angular/core": "5.2.10", + "@angular/forms": "5.2.10", + "@angular/http": "5.2.10", + "@angular/platform-browser": "5.2.10", + "@angular/platform-browser-dynamic": "5.2.10", + "@ionic-native/badge": "4.17.0", + "@ionic-native/camera": "4.17.0", + "@ionic-native/clipboard": "4.17.0", + "@ionic-native/core": "4.11.0", + "@ionic-native/device": "4.17.0", + "@ionic-native/file": "4.17.0", + "@ionic-native/file-opener": "4.17.0", + "@ionic-native/file-transfer": "4.17.0", + "@ionic-native/globalization": "4.17.0", + "@ionic-native/in-app-browser": "4.17.0", + "@ionic-native/keyboard": "4.17.0", + "@ionic-native/local-notifications": "4.17.0", + "@ionic-native/media-capture": "4.17.0", + "@ionic-native/network": "4.17.0", + "@ionic-native/push": "4.17.0", + "@ionic-native/screen-orientation": "4.17.0", + "@ionic-native/splash-screen": "4.17.0", + "@ionic-native/sqlite": "4.17.0", + "@ionic-native/status-bar": "4.17.0", + "@ionic-native/web-intent": "4.17.0", + "@ionic-native/zip": "4.17.0", + "@ngx-translate/core": "8.0.0", + "@ngx-translate/http-loader": "2.0.1", + "@types/cordova": "0.0.34", + "@types/cordova-plugin-file-transfer": "0.0.3", + "@types/cordova-plugin-globalization": "0.0.3", + "@types/cordova-plugin-network-information": "0.0.3", + "@types/node": "8.10.19", + "@types/promise.prototype.finally": "2.0.2", + "chart.js": "2.7.2", + "com-darryncampbell-cordova-plugin-intent": "1.1.7", "cordova": "8.1.2", "cordova-android": "7.1.2", - "cordova-android-support-gradle-release": "^3.0.0", - "cordova-clipboard": "^1.2.1", + "cordova-android-support-gradle-release": "3.0.1", + "cordova-clipboard": "1.3.0", "cordova-ios": "4.5.5", - "cordova-plugin-badge": "^0.8.8", - "cordova-plugin-camera": "^4.0.3", - "cordova-plugin-customurlscheme": "^4.3.0", - "cordova-plugin-device": "^2.0.2", - "cordova-plugin-file": "^6.0.1", + "cordova-plugin-badge": "0.8.8", + "cordova-plugin-camera": "4.1.0", + "cordova-plugin-customurlscheme": "4.4.0", + "cordova-plugin-device": "2.0.3", + "cordova-plugin-file": "6.0.2", "cordova-plugin-file-opener2": "2.0.19", - "cordova-plugin-file-transfer": "^1.7.1", - "cordova-plugin-globalization": "^1.11.0", - "cordova-plugin-inappbrowser": "^3.0.0", - "cordova-plugin-ionic-keyboard": "^2.1.3", + "cordova-plugin-file-transfer": "1.7.1", + "cordova-plugin-globalization": "1.11.0", + "cordova-plugin-inappbrowser": "3.1.0", + "cordova-plugin-ionic-keyboard": "2.1.3", "cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle", - "cordova-plugin-media-capture": "^3.0.2", - "cordova-plugin-network-information": "^2.0.1", - "cordova-plugin-screen-orientation": "^3.0.1", - "cordova-plugin-splashscreen": "^5.0.2", - "cordova-plugin-statusbar": "^2.4.2", - "cordova-plugin-whitelist": "^1.3.3", - "cordova-plugin-zip": "^3.1.0", - "cordova-sqlite-storage": "^2.6.0", - "cordova-support-google-services": "^1.2.1", - "es6-promise-plugin": "^4.2.2", - "font-awesome": "^4.7.0", + "cordova-plugin-media-capture": "3.0.3", + "cordova-plugin-network-information": "2.0.2", + "cordova-plugin-screen-orientation": "3.0.2", + "cordova-plugin-splashscreen": "5.0.3", + "cordova-plugin-statusbar": "2.4.3", + "cordova-plugin-whitelist": "1.3.4", + "cordova-plugin-zip": "3.1.0", + "cordova-sqlite-storage": "2.6.0", + "cordova-support-google-services": "1.2.1", + "es6-promise-plugin": "4.2.2", + "font-awesome": "4.7.0", "ionic-angular": "3.9.3", - "ionicons": "^3.0.0", - "jszip": "^3.1.5", - "moment": "^2.22.2", - "nl.kingsquare.cordova.background-audio": "^1.0.1", - "phonegap-plugin-multidex": "^1.0.0", + "ionicons": "3.0.0", + "jszip": "3.1.5", + "moment": "2.22.2", + "nl.kingsquare.cordova.background-audio": "1.0.1", + "phonegap-plugin-multidex": "1.0.0", "phonegap-plugin-push": "git+https://github.com/moodlemobile/phonegap-plugin-push.git#moodle-v3", - "promise.prototype.finally": "^3.1.0", - "rxjs": "^5.5.11", - "sw-toolbox": "^3.6.0", - "ts-md5": "^1.2.4", - "web-animations-js": "^2.3.1", - "zone.js": "^0.8.26" + "promise.prototype.finally": "3.1.0", + "rxjs": "5.5.11", + "sw-toolbox": "3.6.0", + "ts-md5": "1.2.4", + "web-animations-js": "2.3.1", + "zone.js": "0.8.26" }, "devDependencies": { "@ionic/app-scripts": "3.2.2", - "electron-builder-lib": "^20.23.1", - "electron-rebuild": "^1.8.1", + "electron-builder-lib": "20.23.1", + "electron-rebuild": "1.8.1", "gulp": "4.0.2", - "gulp-clip-empty-files": "^0.1.2", - "gulp-concat": "^2.6.1", - "gulp-flatten": "^0.4.0", - "gulp-rename": "^1.3.0", - "gulp-slash": "^1.1.3", - "gulp-util": "^3.0.8", - "node-loader": "^0.6.0", - "through": "^2.3.8", - "typescript": "^2.6.2", - "webpack-merge": "^4.1.2" + "gulp-clip-empty-files": "0.1.2", + "gulp-concat": "2.6.1", + "gulp-flatten": "0.4.0", + "gulp-rename": "1.3.0", + "gulp-slash": "1.1.3", + "gulp-util": "3.0.8", + "node-loader": "0.6.0", + "through": "2.3.8", + "typescript": "2.6.2", + "webpack-merge": "4.1.2" }, "browser": { "electron": false From 34f7d03334697cc95c53097dc230dc7630899754 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 30 Aug 2019 11:46:58 +0200 Subject: [PATCH 239/241] MOBILE-3137 scripts: List npm packages before compiling --- scripts/aot.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/aot.sh b/scripts/aot.sh index 4a2d344e26a..6a629cb741b 100755 --- a/scripts/aot.sh +++ b/scripts/aot.sh @@ -1,5 +1,8 @@ #!/bin/bash +# List the installed libraries so we can check everything is fine. +npm list + # Compile AOT. if [ $TRAVIS_BRANCH == 'integration' ] || [ $TRAVIS_BRANCH == 'master' ] || [ $TRAVIS_BRANCH == 'desktop' ] || [ -z $TRAVIS_BRANCH ] ; then cd scripts From 9139d1a422d69746c4d17853a7b623b826ae5245 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 30 Aug 2019 12:02:00 +0200 Subject: [PATCH 240/241] MOBILE-3137 scripts: Only list first level of libraries --- scripts/aot.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/aot.sh b/scripts/aot.sh index 6a629cb741b..931d8e4b56b 100755 --- a/scripts/aot.sh +++ b/scripts/aot.sh @@ -1,7 +1,7 @@ #!/bin/bash -# List the installed libraries so we can check everything is fine. -npm list +# List first level of installed libraries so we can check the installed versions. +npm list --depth=0 # Compile AOT. if [ $TRAVIS_BRANCH == 'integration' ] || [ $TRAVIS_BRANCH == 'master' ] || [ $TRAVIS_BRANCH == 'desktop' ] || [ -z $TRAVIS_BRANCH ] ; then From 3edc42ccc2a44e17a9672e4a8973fcbbd8565d3a Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Fri, 30 Aug 2019 11:27:00 +0100 Subject: [PATCH 241/241] MOBILE-3068 release: Fix definitive release version number --- src/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.json b/src/config.json index 0b7d683b8c6..30d79ac4b9b 100644 --- a/src/config.json +++ b/src/config.json @@ -3,7 +3,7 @@ "appname": "Moodle Mobile", "desktopappname": "Moodle Desktop", "versioncode": 3710, - "versionname": "3.7.1-dev", + "versionname": "3.7.1", "cache_update_frequency_usually": 420000, "cache_update_frequency_often": 1200000, "cache_update_frequency_sometimes": 3600000,