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 1c87d2aed..30feb6622 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 b4d8cba41..758784848 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 186608e98..91e4d07cd 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 4d53ac2d3..be1e56919 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 91b804947..2da3aff71 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 762c0709c..97284d658 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 91e4d07cd..f3db2044c 100644 --- a/config.xml +++ b/config.xml @@ -113,33 +113,33 @@ - - + + - - - - + + + + - - + + - - - - + + + + - - - - - - - - - + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 24f38ee3f..2314b5320 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 2da3aff71..94337dbbf 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 | 864 ++++++++++++++++++++++++++++++++++++++-------- package.json | 2 +- 2 files changed, 723 insertions(+), 143 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2314b5320..7288e5926 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": { @@ -5673,6 +5674,549 @@ } } }, + "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" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "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": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "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" + } + }, + "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": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true + } + } + }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", @@ -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 94337dbbf..7ad1f91ae 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 79f1005d0..f456779df 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 36a6110c3..919afa355 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 899978d4b..631885cbe 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 f71c50586..f76eb8b52 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 244c55fcd..16d77cd6e 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 a97b30a2a..c1a5124e4 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 fbb65afba..67159b012 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 0a7b95247..04a978423 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 010dcdbb2..dc688e1c4 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 138e4b4c7..43c1468e5 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 bcaf759e0..144acad5c 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 8a36cbbc0..190e575ac 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 189fad7a3..0670b322b 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 039b9ed56..fe19563b8 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 a99280f3a..da08f33da 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 3b04349f6..7cdbaadac 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 1c262f601..aebf2e8cd 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 869e9461b..b916798c6 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 974c6ddef..400bd4eb1 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 a80b90ade..8fd8982d9 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 c7a37d061..cd9ce8015 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 5fa6c41a7..37e44e7d7 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 bfd8b47b2..6e9f77e91 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 b2a153add..e357d0888 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 1c87d2aed..0cc337fd4 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 c2d67c3f1..6ddb8b33b 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 bb4a55c5a..ae8ab44df 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 04a978423..c458b999c 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 dc688e1c4..5ffe35cb6 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 834629eac..bbea7ec34 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 26f00164e..aaa0d7ab5 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 5df58c9dd..22d925f6a 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 32ab28ac1..1f34a82b0 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 1b8e61de9..20d1d3a49 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 2d4bbc62f..dd54ecac6 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 5f8fe59ed..d601956c4 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 eb2cb5906..f0e15ad31 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 b9bb3eddd..fb9971f02 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 faaa55c81..eb9a0b0b6 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 650cc55e8..fe75a177a 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 7381c6fc8..6f1b7f091 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 7ed0d4812..6069bde0b 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 123d340c1..5a4dda4b3 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 deeb76a53..914d97ab6 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 bf2f305e5..fe9102832 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 0cc337fd4..f296406f5 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 6ddb8b33b..efbd0d748 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 59fdabe77..a1254bad9 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 | 88 +++++++----- .../rich-text-editor/rich-text-editor.ts | 130 ++++++++++++++++-- 3 files changed, 256 insertions(+), 70 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 7a02610dd..40e06d3f4 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 8bce984e8..0cf3a5960 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,33 +59,64 @@ 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; + 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; - 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; + &.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 a1254bad9..f66f553f1 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 40e06d3f4..7874c0162 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 f66f553f1..45e572d37 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 7ad1f91ae..fc78bb983 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 5fa706e64..4a2d344e2 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 571102f5b..000000000 --- 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 000000000..56158a469 --- /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 bc6d128e5..e82eb9523 100644 --- a/scripts/moodle_to_json.php +++ b/scripts/moodle_to_json.php @@ -26,6 +26,10 @@ define('MOODLE_INTERNAL', 1); 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 @@ if (isset($argv[1]) && !empty($argv[1])) { $languages = $config_langs; } -// Process the index file, just once. -$keys = file_get_contents('langindex.json'); -$keys = (array) json_decode($keys); +$keys = get_langindex_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"; - -$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)); + $added_langs = build_languages($new_langs, $keys, $added_langs); } } -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))); - } -} - -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 7381c6fc8..57d11d064 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 2c069ca78..fbbb27c78 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 @@

{{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 0150edbfd..78f439efd 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 3317484cd..c8256d0c4 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 006a78093..52caa2c37 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 7ed0d4812..ef8b0d722 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 0024324c9..752d23a47 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 5f723467c..e0a593b9a 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 a1117bd5d..3af9cf0dd 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 2963017e8..dfadcc542 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 8c699aff0..dcaf4fe01 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 7675876b1..c7abc3485 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 2e89d60fb..8973fbc5d 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 56cbad4f6..46dc2c33b 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 85fe49995..695d8182e 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 fffba5683..faf8aa3be 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 512729b8e..896804473 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 000000000..287592371 --- /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 000000000..a128c44d1 --- /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 47b90ecda..b4e6fcf40 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 7381c6fc8..5abbddcb4 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 7ed0d4812..7608fe277 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 2448c6e1d..764d27fb9 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 896804473..33c80abcf 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 000000000..cf9457d58 --- /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 000000000..8adbb5e4e --- /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 000000000..9b136b8ee --- /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 000000000..c7e298c9c --- /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 5abbddcb4..548a3173f 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 000000000..e3204ae8f --- /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 000000000..86a476c29 --- /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 000000000..9dc4ce2ab --- /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 cd8523d90..7722dc1c4 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 4a174e542..e3b734267 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 7608fe277..cfe806f1d 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 548a3173f..e95ca8279 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 000000000..345d17672 --- /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 000000000..4faba6dd2 --- /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 000000000..b7f4e8acd --- /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 e3b734267..42dfd82d9 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 cfe806f1d..d59c86895 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 e95ca8279..b286cb6b5 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 000000000..bba9d4bc0 --- /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 000000000..c9617c372 --- /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 000000000..f0284df97 --- /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 42dfd82d9..8fd2e06d1 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 d59c86895..93cf84463 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 b286cb6b5..84401eb7f 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 000000000..0a7f81e22 --- /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 000000000..167178d70 --- /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 000000000..fa98be638 --- /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 8fd2e06d1..6203c9b38 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 93cf84463..0ac3f2045 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 84401eb7f..332609914 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 000000000..37d448a93 --- /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 000000000..fe57356da --- /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 000000000..88fca4ec8 --- /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 6203c9b38..0244cbc2c 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 0ac3f2045..d6b15d48c 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 332609914..34a760445 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 000000000..32521695a --- /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 000000000..57d716e5a --- /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 000000000..b101210b2 --- /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 0244cbc2c..594416b75 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 d6b15d48c..8bc1efe9b 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 34a760445..1d142fd09 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 000000000..dce565e7d --- /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 000000000..adcbcabae --- /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 000000000..81e5b2c15 --- /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 594416b75..3086286a1 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 8bc1efe9b..c28b1240e 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 33c80abcf..70627f6bf 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 000000000..84780cabb --- /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 000000000..0acd1712f --- /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 c7e298c9c..7e38227f5 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 bb4a55c5a..3131fc869 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 24ac80e2c..beb6204d0 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 000000000..1b81cf4ad --- /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 000000000..a5603fb53 --- /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 3086286a1..fa19ec008 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 ad0a230fb..d14efd148 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 1d142fd09..a700c40cd 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 000000000..4bc6cd412 --- /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 000000000..0df61ad7a --- /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 000000000..eb05cef2e --- /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 000000000..9967ad6f6 --- /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 fa19ec008..8ce3a1796 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 c28b1240e..4d3f6b00e 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 a700c40cd..20feed1fe 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 000000000..83b981297 --- /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 000000000..a3364ea5e --- /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 000000000..8b0c46287 --- /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 000000000..9b6c15cb8 --- /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 215c343e6..94fe30257 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 f8514dd5f..68598a18e 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 4beb1226f..b975f449c 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 000000000..73d691dc1 --- /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 8ce3a1796..4d4743294 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 4d3f6b00e..88734646f 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 20feed1fe..1c36c87ea 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 000000000..e6d238957 --- /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 000000000..1ae4de38c --- /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 000000000..48b97b5a1 --- /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 ab114c54b..ac311c144 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 000000000..c2f34305c --- /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 4d4743294..6069ee983 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 88734646f..cde20c4dc 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 1c36c87ea..b616afc3c 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 000000000..ff9c450d2 --- /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 000000000..3f9d26d8b --- /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 000000000..dd957321f --- /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 000000000..9cf9b3622 --- /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 6069ee983..1507b6921 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 cde20c4dc..fff9c6689 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 b616afc3c..ceb8559a8 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 000000000..a4080dd78 --- /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 000000000..749188709 --- /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 000000000..4b57911c0 --- /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 000000000..cd3df32eb --- /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 1507b6921..9b4d75af7 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 fff9c6689..390eec79f 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 ceb8559a8..7ee470a71 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 000000000..a9ed3a090 --- /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 000000000..859b876da --- /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 000000000..683c3aa90 --- /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 000000000..cd6ae4c46 --- /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 9b4d75af7..6183c41f3 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 390eec79f..8eba39062 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 7ee470a71..b901aacbb 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 000000000..cbca10200 --- /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 000000000..c649986ed --- /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 000000000..23541f7a0 --- /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 000000000..362b97899 --- /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 6183c41f3..10a4b4b1a 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 8eba39062..b96c9a970 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 b901aacbb..b803d75bf 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 000000000..29f7996e2 --- /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 000000000..043acd495 --- /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 000000000..cd0136763 --- /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 000000000..0eb73e102 --- /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 10a4b4b1a..cc570b6b2 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 b96c9a970..69e66a2b3 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 b803d75bf..95f5d1ca6 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 000000000..1ecdf6f7b --- /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 000000000..81a03b227 --- /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 000000000..a92c0cce5 --- /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 000000000..69140e96d --- /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 cc570b6b2..4c1b3f81f 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 69e66a2b3..a70d49554 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 95f5d1ca6..e29d5b9d7 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 000000000..18282971b --- /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 000000000..ce26caba4 --- /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 000000000..b98c39ae5 --- /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 000000000..33b37b0e7 --- /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 4c1b3f81f..6969e01bc 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 a70d49554..f3f750318 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 7e38227f5..893904ed8 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 b4e6fcf40..7f0f245f3 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 | 112 ++++++++++++------ src/addon/notes/providers/notes.ts | 77 ++++++++++-- 5 files changed, 277 insertions(+), 59 deletions(-) diff --git a/src/addon/notes/components/list/addon-notes-list.html b/src/addon/notes/components/list/addon-notes-list.html index fbbb27c78..7908279f8 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 @@

{{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 78f439efd..e9b6ffd7a 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 486fa0111..28bae97bf 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 77b7039c3..cebf92d73 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) => { - // Get all the courses to be synced. - const courseIds = []; - notes.forEach((note) => { - if (courseIds.indexOf(note.courseid) == -1) { - courseIds.push(note.courseid); - } - }); + 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 = {}; + 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 52caa2c37..82f095e41 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 568eac629..0f7fed557 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 5b17bf4a5..187a6fbba 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 c43a61bc0..1d9e9ea03 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 04f8283ee..68463d4e3 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 2358295de..1ed053f9d 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 e6fbd9256..54aac1ca3 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 a263241d9..82c52cd8e 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 e723bc44a..60ccc28a4 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 3e231ecf4..79be3063c 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 e1fa41f17..4b6356844 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 de4050eb3..f5829666c 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 4e18eef73..4d3128918 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 7381c6fc8..c9808298f 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 8a69c73de..394f78176 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 7ed0d4812..4730a8448 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 bf2f305e5..d780ed3af 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 e1595c75b..77867c500 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 3b8d1751a..1ab97b2be 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 9c28973fb..5617d6c92 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 f05219300..2f8176995 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 e2aad3def..69a578fe0 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 cccfde281..b99cc7760 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 ae04b5574..d544e4e85 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 0ecaa7515..e918ecf11 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 4f91bb0ff..9636dcfeb 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 20b3d2116..3c2089190 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 ce0edcde6..7d4cadb37 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 4114678c4..b7882e1f1 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 2d4bbc62f..5adc9ac32 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 b55378728..2b1242d85 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 09d873f94..aded3de7e 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 5988289a7..722d9eae6 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 d1e927add..807bceba3 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 8cfbd3067..489561674 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 6771a6ed1..2952c22b1 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 d1e927add..807bceba3 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 d87fa7cc4..4fa2cc62b 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 97284d658..0091827b5 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 ea038eece..8884ec530 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 d2b371887..b4eab9bce 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 2358295de..e6b017cc8 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 e6fbd9256..feb6a9855 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 4f4fdab17..1483a6886 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 | 39 +++++++++++++--------- src/addon/mod/scorm/providers/helper.ts | 16 ++++++--- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/addon/mod/scorm/pages/player/player.ts b/src/addon/mod/scorm/pages/player/player.ts index 63d148aca..93024042c 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 (this.newAttempt) { + // Creating a new attempt, use the first SCO defined by the SCORM. + this.initialScoId = this.scorm.launch; + } - 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'; - } - }); + // 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 95dd3c31b..ec0e1523b 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 738c1c0db..a6952f917 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 110a93a72..da3c478fe 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.contactyouradministratorissue' | translate:{$a: ''} }}

-

+

diff --git a/src/core/login/pages/site/site.ts b/src/core/login/pages/site/site.ts index c4bd86fda..8b1fcf600 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 b64da98fd..b01a412d1 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 4f4fdab17..24374f3cd 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 = ''; + const message = this.getErrorMessage(error, needsTranslate); - 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); - - 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 0f7fed557..ece9ca7bf 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 187a6fbba..ff0ab6ded 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 1d9e9ea03..8da732a64 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 e0c5633e1..f7c2de9f6 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 6ccb04caa..3139bb8a9 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 000000000..bfddc7633 --- /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 000000000..c5b20e969 --- /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 000000000..3c43c635e --- /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 000000000..abb8f8b75 --- /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 e5cdc3eb0..97ae57d40 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 7722dc1c4..237ad2457 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 40dcd2208..23f664371 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 a1a85759d..818feadf1 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 0caa80266..2ed0480e9 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 fe9102832..b5c79cb94 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 e1595c75b..0f9f71b3b 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 3c40c931a..d823697c6 100644 --- a/src/addon/calendar/pages/event/event.html +++ b/src/addon/calendar/pages/event/event.html @@ -26,6 +26,10 @@

{{ '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 ece9ca7bf..77d4bce09 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 | 69 ++++-- .../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, 437 insertions(+), 44 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 3146e6e69..c8131878a 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 bfddc7633..0cf401536 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 abb8f8b75..d6a6ec488 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 97ae57d40..50a57e777 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 237ad2457..d799f64c5 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 000000000..c720215bc --- /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 23f664371..6e5bc965b 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 818feadf1..30d4b9521 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 ff0ab6ded..c846032a8 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 f36d166b7..bf9d65079 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 2ed0480e9..14730d64b 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 b5c79cb94..714b64a15 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 f7c2de9f6..020860908 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 | 87 ++++--- 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, 536 insertions(+), 79 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 5b005c6fb..dc86f5c89 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 c8131878a..c8f9cef82 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 3139bb8a9..a7a531836 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 d6a6ec488..bf394c205 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; + // If editing an event, get offline data. Wait for sync first. - // 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; + 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 50a57e777..0204b9acf 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 d799f64c5..1a57c4c0b 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. */ - refreshEvents(refresher?: any): Promise { + 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(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 c720215bc..833ca7d4b 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 000000000..cb9eb4b63 --- /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 30d4b9521..ae857225d 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 000000000..6aabcca74 --- /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 14730d64b..a7884b46f 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 714b64a15..b5c79cb94 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 020860908..6e2e00aa7 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 dc86f5c89..6103e27f7 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 0cf401536..96edde1b9 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 bf394c205..e800800c2 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 d823697c6..93e5bd285 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 77d4bce09..6fb0a16d1 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'; }); } @@ -246,16 +346,77 @@ export class AddonCalendarEventPage { }); } + /** + * 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.eventLoaded) { + return this.refreshEvent(true, showErrors).finally(() => { + refresher && refresher.complete(); + done && done(); + }); + } + + return Promise.resolve(); + } + /** * Refresh the event. * - * @param {any} refresher Refresher. + * @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(refresher: any): void { - this.calendarProvider.invalidateEvent(this.eventId).finally(() => { - this.fetchEvent().finally(() => { - refresher.complete(); - }); + 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 0204b9acf..57563190d 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 1a57c4c0b..3418f0514 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 cb9eb4b63..834d1ac34 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 6e5bc965b..0b9af1e6b 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 a7884b46f..e8a1c8227 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 b5c79cb94..4ec6a60a4 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 0f9f71b3b..95cd5cee1 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 3418f0514..029234a74 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 0b9af1e6b..c04ebaa2a 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 96edde1b9..7330b752e 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 e800800c2..0017a81ea 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 57563190d..a224599f7 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 029234a74..e3ce88a44 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 c04ebaa2a..bfd3a03df 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 6103e27f7..75ad66948 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 a7a531836..d5cf329a2 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 7330b752e..f57a84915 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 3c43c635e..6426ce3f1 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 0017a81ea..97049a034 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 833ca7d4b..a929113bc 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 e8a1c8227..63974e503 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 f57a84915..9d79a7e5d 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 97049a034..edda98102 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 compareA.localeCompare(compareB); + 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); + }); }); })); } @@ -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 93e5bd285..2608581ae 100644 --- a/src/addon/calendar/pages/event/event.html +++ b/src/addon/calendar/pages/event/event.html @@ -40,10 +40,10 @@

{{ '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 | 124 +++++++++---- 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, 628 insertions(+), 81 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 75ad66948..40cfc905d 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 d5cf329a2..8792b48c5 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 2608581ae..23e55aac7 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 000000000..6a9913737 --- /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 6fb0a16d1..9f312a3e8 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 a224599f7..bd5c848ad 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 000000000..9f40d9746 --- /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 e3ce88a44..0dac7f74b 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 a929113bc..3d48baa38 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 834d1ac34..6b81d39ae 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. - - 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. + // 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.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) + // Event sent, delete the offline data. + const promises = []; + + 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 = []; + + 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 bfd3a03df..9e34520a1 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 99bcbec94..41bd0e1ca 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 a9f4f175a..90e59df6d 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 65a21bec8..f1da26ca8 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 68598a18e..7c395fc57 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 63974e503..19cb0008d 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 925f9109b..752a140e2 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 4f4fdab17..9cbfb77f5 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 01f85c86e..e564a97d8 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 fc78bb983..82773693b 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 82773693b..3c1c766ce 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 dc048e446..4f66d9dc1 100644 --- a/src/addon/competency/pages/competency/competency.html +++ b/src/addon/competency/pages/competency/competency.html @@ -76,7 +76,7 @@ {{ '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 dc7e76c9d..c3930b945 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 0ff335202..c46b8942e 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.userswhoneedtosubmit' | translate: {$a: ''} }}

- +

{{ user.fullname }}

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

{{ '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 c3f655855..9d5fd32d2 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 cb8ffc1b3..9119c2bed 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 581c7eefe..a8d010a9e 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 298ace24a..22c6fe5a2 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 d1714bacb..981e489b1 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 c8f9cef82..66ab9f237 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 000000000..122436442 --- /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 000000000..0c3486dd9 --- /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 000000000..3c1be5ed2 --- /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 9e34520a1..fdc4905d3 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 2769f0e0b..0568eeb01 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 f456779df..f50f047fe 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 919afa355..448394214 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 40cfc905d..2f79cd0a0 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 000000000..04d35dfda --- /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 000000000..853cff23a --- /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 000000000..9bb41721e --- /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 000000000..a6d5afe22 --- /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 8792b48c5..412c81742 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 122436442..42c3cd4b9 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 0c3486dd9..bf925e799 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 3c1be5ed2..9ab19cd7e 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 0dac7f74b..00c6657d4 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 fdc4905d3..4705a8ad8 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 ae857225d..94cfa1e93 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 19cb0008d..3e4a0a984 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 4ec6a60a4..4f1d092cf 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 919afa355..0062ebe09 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 04d35dfda..a866c4be2 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 853cff23a..c3a20bf9e 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 9bb41721e..053863483 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,27 +101,63 @@ 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); + }); + + // Classify them by month. + 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; + })); + + 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'); @@ -104,13 +165,11 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.weekDays = this.calendarProvider.getWeekDays(result.daynames[0].dayno); this.weeks = result.weeks; - this.filterEvents(courseId, categoryId); - })); + // Merge the online events with offline data. + this.mergeEvents(); - return Promise.all(promises).catch((error) => { - this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); - }).finally(() => { - this.loaded = true; + // Filter events by course. + this.filterEvents(); }); } @@ -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 42c3cd4b9..fa0877b46 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 9ab19cd7e..8f134ed90 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'; - return Promise.all(promises).catch((error) => { + 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 = []; + + 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 3d48baa38..ee06f75c9 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 94cfa1e93..225ec7ffb 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 7874c0162..c4b96e0fe 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 45e572d37..44be93408 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 0cf3a5960..9460297fd 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 44be93408..b0261ba75 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 c4b96e0fe..ba2bd897f 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 6652b879b..e9533a7d2 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 9808a0c83..cd671d84d 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 c4111f1be..3aceb76c7 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 b66db02a3..d737c8e1b 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 51e95ce9c..2a938cbb1 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 fed935d1d..a0146c85a 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 e9533a7d2..74f31663a 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 cd671d84d..0279c711e 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 980e458b8..3dc800d05 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 a0146c85a..b380d8f32 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 8642ffbb0..2c2c8efeb 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 41e56725e..cb825c4dd 100644 --- a/src/core/comments/pages/viewer/viewer.html +++ b/src/core/comments/pages/viewer/viewer.html @@ -20,9 +20,11 @@ + + - + diff --git a/src/core/comments/pages/viewer/viewer.ts b/src/core/comments/pages/viewer/viewer.ts index 74f31663a..d253fbcb8 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 0279c711e..e5634bf26 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 42b0102c1..4ada12bc3 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 3dc800d05..d19d2ccf3 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 b380d8f32..6f7a22444 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 0eb280877..b6c99e4c3 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 000000000..b5ca49440 --- /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 000000000..a6b6661a0 --- /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 000000000..1d58db46f --- /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 cb825c4dd..812838695 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 @@
- diff --git a/src/core/comments/pages/viewer/viewer.module.ts b/src/core/comments/pages/viewer/viewer.module.ts index ca5267970..3326cfe19 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 d253fbcb8..50bbf004f 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(() => { - this.comments = this.comments.concat(comments); + // 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; + 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 e5634bf26..a17748b08 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 000000000..82dbd6894 --- /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 000000000..5803f936e --- /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 000000000..ab43a6d4b --- /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 | 223 ++++++++++++++++--- src/core/comments/providers/sync.ts | 82 ++++--- 9 files changed, 451 insertions(+), 108 deletions(-) diff --git a/src/addon/blog/components/entries/entries.ts b/src/addon/blog/components/entries/entries.ts index d737c8e1b..804538d85 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 4ada12bc3..5b481b28a 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 b6c99e4c3..c48dcce17 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 1d58db46f..66510fb79 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 812838695..2a023a419 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 @@

{{ 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 50bbf004f..0f606feee 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 a17748b08..eb5613c45 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 82dbd6894..94caf9fb3 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' + } + ] } ] }; @@ -73,30 +104,6 @@ export class CoreCommentsOfflineProvider { 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. * @@ -105,7 +112,10 @@ export class CoreCommentsOfflineProvider { */ getAllComments(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_TABLE); + return Promise.all([site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_TABLE), + site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE)]).then((results) => { + return [].concat.apply([], results); + }); }); } @@ -136,19 +146,116 @@ export class CoreCommentsOfflineProvider { } /** - * Check if there are 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. - * @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 with the comments. */ - 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; + 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. + */ + getAllDeletedComments(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_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. + */ + getDeletedComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '', + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_DELETED_TABLE, { + contextlevel: contextLevel, + instanceid: instanceId, + component: component, + itemid: itemId, + area: area + }); + }).catch(() => { + return false; + }); + } + + /** + * 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. + * @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 + }); + }); + } + + /** + * 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 ab43a6d4b..c8466cac7 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 = []; - if (comment.action == 'add') { - promise = this.commentsProvider.addCommentOnline(comment.content, contextLevel, instanceId, component, itemId, area, - siteId); + 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 (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 9253f83e6..47a64ffe8 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 0cc906f5a..08d53055c 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 5351ef509..89bf47b41 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 6969e01bc..f908025de 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 21810db95..0dc92ff13 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 000000000..c2e07f85b --- /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 000000000..7e6372e20 --- /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 000000000..569d645d6 --- /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 000000000..6abbf3d7f --- /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 000000000..fec56bb36 --- /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 000000000..fdcdaf189 --- /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 000000000..eaa2f9e81 --- /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 0e56fcc3f..efba08d7d 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 670e4d276..690930091 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 b66db02a3..a0e4bd3da 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 54e83ef50..4cc338b6e 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 6f4e0be3a..13cc19b7e 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 1b154d373..569060aa3 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 1655ed164..74fd32706 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 9e96c3d1a..b96dc078d 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 41a44e5fa..b6c9e9924 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 3470ae872..ef12a46b3 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 b5aaadcec..477eed3fc 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 89bf47b41..d9741522c 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 f358c48a8..f7c0005dc 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 35e74cd06..68d6cdada 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 4ca20b5d0..fb0a16495 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 0dc92ff13..b75231f80 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 0f3bf1b10..06cdabd01 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 9119c2bed..81107408c 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 a8d010a9e..d6a9ca54d 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 f34e46d27..5954398ff 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 cc69e9dc4..730943091 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 7bbf4f0bf..a5b38641d 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 39372cfe2..638be75aa 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 3d8834595..2ee9b5e8d 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 941417a68..7b4a69f16 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 c2e07f85b..8960002cd 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 000000000..fe4a02e21 --- /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 000000000..5c554f7c3 --- /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 000000000..2b0e2ffb8 --- /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 000000000..38c097b79 --- /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 eaa2f9e81..45d4d69af 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 d9741522c..b9db7a280 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 b75231f80..1174b5e6a 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 a56f920d8..4b55708e2 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 000000000..b372fdf09 --- /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 000000000..07d34c21c --- /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 5293eda63..4e8206abf 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 000000000..066754de6 --- /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 fec56bb36..c8303131c 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 b9db7a280..f36b31fdf 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 1174b5e6a..3dc88e71c 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 4e8206abf..d14844582 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 000000000..4b7dcfc0a --- /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 c8303131c..3275d27af 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 f36b31fdf..c40287ac5 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 f31372745..b697ba27a 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 000000000..41413d824 --- /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 3dc88e71c..344ae3a2c 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 3275d27af..ce6aec9f1 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 c40287ac5..a0dbf92e1 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 344ae3a2c..f48252c14 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 ce6aec9f1..02e288849 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 7e7427c17..e741ad964 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 000000000..8ca11b857 --- /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 000000000..8c4f01612 --- /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 000000000..ab2d167cf --- /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 845f2179e..0370243a6 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 a0dbf92e1..57c4bf18d 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 cc06e008d..ea425d1c8 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 7d1140fe4..4f8f32f54 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 000000000..47c456cb6 --- /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 f48252c14..121bd0dba 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 57c4bf18d..ac7fabe30 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 9babfa364..158865aec 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 f7c0005dc..219ec090c 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 000000000..f7fb15098 --- /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 121bd0dba..ba1bfb778 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 ac7fabe30..cb110da9c 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 94fe30257..b8463c714 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 93e72d2c2..dbfac5fd3 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 000000000..02505b31c --- /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 ba1bfb778..cb31d3779 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 cb110da9c..2d1efd4f0 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 ac311c144..e3520fbe7 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 18e5ff7bc..ba4329f33 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 000000000..4c62f698d --- /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 cb31d3779..00a011891 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 2d1efd4f0..369488b84 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 29ed054ce..246965b9c 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 000000000..0f3cab521 --- /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 539b962dd..6396cac74 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 00a011891..6fc2be13d 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 369488b84..679e4d559 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 6fc2be13d..69730eab9 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 02e288849..b7f8b8794 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 000000000..8a43d2d51 --- /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 000000000..87a49cd7f --- /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 000000000..9f71f0532 --- /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 000000000..5174fcd7c --- /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 000000000..bb3cd138d --- /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 000000000..9185bae0f --- /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 fdcdaf189..88b739f04 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 61922cbb3..e978908d1 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 679e4d559..10ac601e4 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 69730eab9..726ea14f2 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 b7f8b8794..c23afc5e9 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 000000000..5635d09d9 --- /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 000000000..29776ce68 --- /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 000000000..cd7173445 --- /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 000000000..13f09bb4c --- /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 000000000..8e676acc1 --- /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 88b739f04..3a377cba9 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 45d4d69af..baa55d98f 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 000000000..c8e1d7c49 --- /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 000000000..68ea7cb96 --- /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 baa55d98f..970e66e46 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 489561674..5933f59c3 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 0540eb83f..a10f69fde 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 f3db2044c..211280ed8 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 558eac531..ed3d807e9 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 bd6862d68..cd494187f 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 ba5be62f3..47369498e 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 9253f83e6..37ae0616a 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 b36202f7e..d257764c2 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 2d962d17c..92697e4f0 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 e1a3c10a5..f9a5b7f82 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 0f3355219..23bfb74ca 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 ffa235134..ca25f3354 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 faf8aa3be..eebf4fd4d 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 f9eff8228..d94fcdd0a 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 a8a1ed1bb..17a52f8b0 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 34aa44057..a629f76c4 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 37a806f43..69d6fdcbf 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 35e74cd06..52025cfa3 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 51e95ce9c..b8c7da5dd 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 4ca20b5d0..036f517b7 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 b5aaadcec..e68275545 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 e68275545..a425a5cc6 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 24374f3cd..4a9ad7ed6 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 211280ed8..744ea653e 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 c50cc9656..c63780c5c 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 | 14 ++-- .../classes/handlers/course-option-handler.ts | 34 +++++++- .../classes/handlers/user-handler.ts | 25 +++++- src/core/siteplugins/providers/helper.ts | 82 +++++++++++++++++-- src/providers/events.ts | 1 + 5 files changed, 136 insertions(+), 20 deletions(-) diff --git a/src/core/course/providers/options-delegate.ts b/src/core/course/providers/options-delegate.ts index b99cc7760..8f0ac6124 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 e918ecf11..e3fde2bd2 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 044fd2317..5bd01b063 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 2b1242d85..ded88ec73 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); + prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title), + handler = new CoreSitePluginsCourseOptionHandler(uniqueName, prefixedTitle, plugin, + handlerSchema, initResult, this.sitePluginsProvider, this.utils); - this.courseOptionsDelegate.registerHandler(new CoreSitePluginsCourseOptionHandler(uniqueName, prefixedTitle, plugin, - handlerSchema, initResult, this.sitePluginsProvider)); + 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); + prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title), + handler = new CoreSitePluginsUserProfileHandler(uniqueName, prefixedTitle, plugin, handlerSchema, + initResult, this.sitePluginsProvider, this.utils); - this.userDelegate.registerHandler(new CoreSitePluginsUserProfileHandler(uniqueName, prefixedTitle, plugin, handlerSchema, - initResult, this.sitePluginsProvider)); + 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 fe75a177a..48f3dcf32 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 2f79cd0a0..1d2d82d13 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 a866c4be2..007100baf 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 053863483..203db79e8 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 a6d5afe22..3928f6dd9 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 000000000..b338f0eca --- /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 000000000..41751c0d5 --- /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 412c81742..ba81a9a89 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 fa0877b46..120c9dc2c 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 8f134ed90..396690427 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 4705a8ad8..c759210db 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 41759e8ef..7123b9909 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 3e4a0a984..7dae77a70 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 | 66 +- src/addon/calendar/providers/calendar.ts | 99 +++ src/assets/lang/en.json | 2 + src/core/courses/providers/helper.ts | 74 ++- 17 files changed, 963 insertions(+), 92 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 1d2d82d13..d80355727 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 007100baf..7c607ca63 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 c3a20bf9e..af7182c32 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 203db79e8..2b40b8b3f 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 41751c0d5..f358b5785 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 ba81a9a89..8d19b4559 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 000000000..e9b48bd52 --- /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 000000000..c4e33dbb9 --- /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 000000000..924bfa0e7 --- /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 edda98102..93f8b3499 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 9f312a3e8..bfb45499c 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 120c9dc2c..099892df8 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 396690427..59088156b 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 00c6657d4..1c7572e12 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.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.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; - }); + 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 c759210db..3bb868034 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 7dae77a70..b78160d84 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 6ff3ba5cf..abd9634fe 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 | 44 +++---- .../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, 276 insertions(+), 80 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 66ab9f237..28765fad6 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 7c607ca63..ea373d297 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 2b40b8b3f..2a94768f8 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; - }); - } + // Format online events. + day.events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); - 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 || this.deletedEvents.length) { + // There is offline data, merge it. - if (monthOfflineEvents && monthOfflineEvents[day.mday]) { - // Add the offline events (either new or edited). - day.events = this.sortEvents(day.events.concat(monthOfflineEvents[day.mday])); + 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 b338f0eca..0a4b7bb1e 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 f358b5785..d362b6d6e 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 e9b48bd52..134def736 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 924bfa0e7..43be54b5b 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 23e55aac7..6e64b6fe0 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 bfb45499c..7af265152 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 099892df8..ca8e832f3 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 59088156b..341cf36b3 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 1c7572e12..0b864a78a 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 3bb868034..e7c0800f3 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) + ', '; + const timeStart = this.timeUtils.userDate(start, format), + timeEnd = this.timeUtils.userDate(end, format), + promises = []; - if (showTime == midnightStart) { - dayStart = ''; + // 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 (dayEnd && (!seenDay || !moment(seenDay).isSame(end, 'day'))) { + promises.push(this.getViewUrl('day', end / 1000, undefined, siteId).then((url) => { + dayEnd = this.urlUtils.buildLink(url, dayEnd); + })); } - 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 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 225ec7ffb..575500c16 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 000000000..e0ba641c0 --- /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 3035f53c8..70bd15d55 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 d80355727..aa47c8821 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 ea373d297..1cc977fdf 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 af7182c32..b764187e4 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 8d19b4559..be2bab15a 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 6e64b6fe0..366327f66 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.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 7af265152..fb9354748 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 b78160d84..a8564ee92 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 2a94768f8..cbeeb6379 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 d362b6d6e..d540dae6e 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 43be54b5b..d18e25d75 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 9dc4ce2ab..80e85edf8 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 b7f4e8acd..c58a0d080 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 1cc977fdf..679b0996e 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 b764187e4..f96cba14d 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 134def736..e6db57d36 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 000000000..4918c1312 --- /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 0062ebe09..9b5f9ae66 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 679b0996e..5872e86d0 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 cbeeb6379..dcdb412b1 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 e6db57d36..0ef39ef72 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 d18e25d75..a8c1e90a1 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 ca8e832f3..adfbe89db 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 8b419c87c..cf2744238 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 efbd0d748..d787aae14 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 17a52f8b0..389a968ac 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 a629f76c4..8c8a511c1 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 019cd4657..68937d899 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 489561674..7cba685c7 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); + }); + + img.parentNode.appendChild(anchor); }); - - container.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 7cba685c7..2f851c361 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 9f6514ba6..46d2a8600 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 55686efd0..eae6da52c 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 410482e76..c08daa491 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 fb9354748..5789fb574 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 e7c0800f3..eea082da3 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'; + let oldTable = 'addon_calendar_events_2'; - return db.tableExists(oldTable).then(() => { + return db.tableExists(oldTable).catch(() => { + // The v2 table doesn't exist, try with v1. + oldTable = 'addon_calendar_events'; + + 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 now = Math.round(Date.now() / 1000); + const promises = []; - return Promise.all(events.map((event) => { - if (event.notificationtime == 0) { - // No reminders. - return Promise.resolve(); - } - - let time; - - if (event.notificationtime == -1) { - time = -1; - } else { - time = event.timestart - event.notificationtime * 60; - - if (time < now) { - // Old reminder, just not add this. - return Promise.resolve(); - } - } - - 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 55cc163c8..655de6301 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 8a0acdba1..9c3458b72 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 598c204f0..294a499c4 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.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 b36202f7e..4848e4d5c 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 9cf9b3622..d6cfb677a 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 362b97899..231137b8e 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 69140e96d..55f03cb8e 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 cd6ae4c46..aa2a17495 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 48b97b5a1..d639ccb16 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 9b6c15cb8..c077474d8 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 9967ad6f6..353835c83 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 043acd495..ac69af02b 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 ce26caba4..8976f2182 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 749188709..f1c7d4cd8 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 84780cabb..84cf2ac52 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 8ee9dd059..b4734d36e 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 807bceba3..c3375a90f 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 ebc7d03b9..abba3cd8c 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 000000000..287592371 --- /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 000000000..b0ceaa456 --- /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 1ca20e3f2..f046cdbb4 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 97284d658..47588bf5f 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 bc403c9e0..8a70e2dfc 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 055b3b689..be9ec43d9 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 bb204717c..7f7cb5b74 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.noquestions' | translate }}

- +

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

{{ type }}

@@ -109,6 +109,13 @@ {{ '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 5872e86d0..073811937 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 @@ - -

{{ day.mday }}

+ +

{{ day.mday }}

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

+

@@ -56,7 +56,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.ts b/src/addon/calendar/components/calendar/calendar.ts index dcdb412b1..5507a389f 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 073811937..1bf31b5d0 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 083bdd315..cec0955ac 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 7f4ae239b..c38931322 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 d67c7fb90..e0a99c94c 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 1bf31b5d0..bf5c29422 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 @@ -

{{ 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 f96cba14d..278e56bcf 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 1a83ac4fe..00005972a 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 9ddb17e59..f812ef2b6 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 8e8a0ebd3..31f667650 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 2d0a1baa0..30989ecad 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 8e8a0ebd3..6470d8a4f 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 ac4b2ac54..01568394d 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 b752f8d5e..9befe638a 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 a094ae0ff..1727a6d28 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 2d0a1baa0..cf34c2ee7 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 | 104 ++++++++++++++----------- 1 file changed, 58 insertions(+), 46 deletions(-) diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index 00005972a..4ac36ec70 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 = []; + 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, siteId) - .then((events) => { - return events.map((event) => { - timestarts.push(event.timestart); + 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); + 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--; - } - - invalidatePromise = Promise.resolve(); - } - } else { - // Not repeated. - timestarts.push(event.timestart); - invalidatePromise = this.calendarProvider.invalidateEvent(event.id); - } - - return invalidatePromise.then(() => { - let lastMonth, lastYear; - - return Promise.all([ - this.calendarProvider.invalidateAllUpcomingEvents(), - timestarts.map((time) => { - const day = moment(new Date(time * 1000)); - - if (lastMonth && (lastMonth == day.month() + 1 && lastYear == day.year())) { - return Promise.resolve(); + } else { + // Being added. + let time = event.timestart; + while (repeated > 0) { + timestarts.push(time); + time += CoreConstants.SECONDS_DAY * 7; + repeated--; } - // Invalidate once. - lastMonth = day.month() + 1; - lastYear = day.year(); + invalidatePromise = Promise.resolve(); + } + } else { + // Not repeated. + timestarts.push(event.timestart); + invalidatePromise = this.calendarProvider.invalidateEvent(event.id); + } - return this.calendarProvider.invalidateMonthlyEvents(lastYear, lastMonth, siteId); - }) - ]); + return invalidatePromise.finally(() => { + let lastMonth, lastYear; + + return this.utils.allPromises([ + this.calendarProvider.invalidateAllUpcomingEvents(), + + // 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(); + + return this.calendarProvider.invalidateMonthlyEvents(lastYear, lastMonth, site.id); + })), + + // 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 a8c1e90a1..f5a302997 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 1ea76b5ba..6d03d0b22 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 da6737465..8c5698b50 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 5507a389f..a620fe6ee 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 f5a302997..73202cfd3 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 9ecc4effd..5ffe6d29d 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 4ac36ec70..c3b24c956 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 d540dae6e..74db1aebc 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 5ffe6d29d..7b09df4a8 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 adfbe89db..fc6386e36 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 e9f9e228a..cc2c9f3c0 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 744dd4326..574dc3c8b 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 028dc2785..2d01ef5a1 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 389a968ac..7bc8292fe 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 e0a99c94c..8776024e7 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 d14efd148..a14ee433e 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 c2f34305c..c86557aee 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 953aeb41a..99acce7b7 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 | 146 ++++++++++---------- src/addon/block/tags/tags.scss | 176 +++++++++++++------------ 2 files changed, 167 insertions(+), 155 deletions(-) diff --git a/src/addon/block/blogtags/blogtags.scss b/src/addon/block/blogtags/blogtags.scss index 859b876da..a974b45cb 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; + padding: .2em; + display: inline-block; + + a { + @extend ion-badge; + @extend .badge-md; + text-decoration: none; + } + .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; + } } } - .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/tags/tags.scss b/src/addon/block/tags/tags.scss index cd3df32eb..f4c54d167 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 81e5b2c15..ada6e1654 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 0f606feee..e84d27e0c 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 6b81d39ae..457c25bb3 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 f2e7d5892..8bbc87959 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 d9977a388..ada6cea19 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 f812ef2b6..d15793574 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 9409f2ee0..ef69785f5 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 a8e23425b..454c21834 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 6d03d0b22..54e78480c 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 000000000..213b8abe5 --- /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 7b09df4a8..17f372590 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 ada6cea19..ea7b02b9a 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 f2808c627..2daaf90cf 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 2daaf90cf..99b887ac8 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 65a7a6f66..29d3d321e 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 6b16d9401..31f667650 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 6e2e00aa7..9be16dd58 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 | 117 +++++++++++------- 5 files changed, 126 insertions(+), 66 deletions(-) diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index 2daaf90cf..91f8a505a 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 2d4811227..d23579df0 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 ee06f75c9..5b0f02436 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 457c25bb3..b395ae6af 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 c3b24c956..36d1f6518 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); + // Invalidate the events and get the timestarts so we can invalidate months & days. + return this.utils.allPromises(events.map((eventData) => { - 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--; + 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; + + promises.push(this.calendarProvider.invalidateMonthlyEvents(day.year(), day.month() + 1, site.id)); } - // Invalidate once. - lastMonth = day.month() + 1; - lastYear = day.year(); + if (!invalidatedDays[dayId]) { + // Day not invalidated already, do it now. + invalidatedDays[dayId] = dayId; - return this.calendarProvider.invalidateMonthlyEvents(lastYear, lastMonth, site.id); - })), + promises.push(this.calendarProvider.invalidateDayEvents(day.year(), day.month() + 1, day.date(), + site.id)); + } - // 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); - })), + 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 eb791bab4..fbf6eb574 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 | 35 ++++++++----------- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 1be81e471..cd0200f8d 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 7d4cadb37..44955e85a 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 ba08c11be..c653e51a6 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 a80aca762..c52b205a5 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 = []; + + if (this.currentSite) { + const siteConfig = this.currentSite.getStoredConfig(); + siteId = this.currentSite.getId(); + + this.currentSite = undefined; + + if (siteConfig && siteConfig.tool_mobile_forcelogout == '1') { + promises.push(this.setSiteLoggedOut(siteId, true)); + } + + promises.push(this.appDB.deleteRecords(this.CURRENT_SITE_TABLE, { id: 1 })); } - const siteId = this.currentSite.getId(), - siteConfig = this.currentSite.getStoredConfig(), - promises = []; - - this.currentSite = undefined; - - if (siteConfig && siteConfig.tool_mobile_forcelogout == '1') { - promises.push(this.setSiteLoggedOut(siteId, true)); - } - - 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 7fa661716..fe20b3115 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 2655a2276..8eeaf67a3 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 8a7745585..bee372275 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 8bbc87959..0b52e8337 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 1727a6d28..b2ac149d1 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 5d457bfd8..921143228 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 9006b3011..fc4aaf2fc 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 574dc3c8b..7db1ffa8c 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 | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 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 8a7dae440..4c8be80bf 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 e01cb15cc..07bf12cca 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 a80aca762..9588a3e60 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 d6a08f6e9..d0483b68a 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 262bb8e3b..0a79a4766 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 07bf12cca..18be199e1 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 | 25 +++++++++------ .../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, 86 insertions(+), 27 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 454c21834..8819689b2 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 259c6c729..37a1cbeb2 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 11e53d2b2..3b793da19 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 }); + // 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' }); - popover.onDidDismiss((itemId) => { - if (!itemId) { - // Not valid, probably a category. - return; + 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 6bda8d594..000000000 --- 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 4abb95089..441eea598 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 000000000..035a5d6a0 --- /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 000000000..28f970142 --- /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 115c1241f..1880e4813 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 54e78480c..8c35aeb95 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 17fe16bb5..0985c6b18 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 9588a3e60..d97901828 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 18be199e1..4f885fe93 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 33e20f0c2..a6f31c3a5 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 22df10cb7..699843c09 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 306c71525..db08cf476 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 08d53055c..636b00e0b 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 55d40cff9..cb1a902ec 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 72b515f38..7913b8d6d 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 ca1e66232..f17702e24 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 80246e1a2..d4dd9a821 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 0b52e8337..1118e0771 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 cfef11512..f8ab362c9 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 2d56bc768..c2a6f23bb 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 14425705d..1f386603b 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 7908279f8..a959b746f 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 8819689b2..fe54142bd 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 bf5c29422..030377251 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 @@ - {{ day.shortname | translate }} + {{ day.shortname | translate }} + {{ day.fullname | translate }} - -

{{ day.mday }}

+ +

{{ day.mday }}

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

+

- {{ 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 278e56bcf..ff2d77c97 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-day-number { - height: 24px; - line-height: 24px; - width: max-content; - min-width: 24px; - text-align: center; - font-weight: 500; - display: inline-block; - margin: 3px; + + &.addon-calendar-event-past-day > .addon-calendar-dot-types, + &.addon-calendar-event-past-day > .addon-calendar-day-events { + opacity: 0.5; } - &.today .addon-calendar-day-number { + + .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 a620fe6ee..985f284c8 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 be2bab15a..ea79d56e5 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 cec0955ac..9cfd3705a 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 @@ - +

@@ -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 73202cfd3..cc930e728 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 fc6386e36..d2d6d38b0 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 213b8abe5..000000000 --- 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 bd5c848ad..2d85b3f1d 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 8c35aeb95..831223df1 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 a14ee433e..8365e7363 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 d14844582..52a686a40 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 f046cdbb4..b6c33d0fc 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 5bcb8e938..515d7117a 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 ee6af5a26..a34ada03c 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 cc2c9f3c0..61fcad7be 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 7db1ffa8c..c1b849c56 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 921143228..bf1b3c6cd 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 690930091..b3a41ae72 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 071e53fe6..89a5b0917 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 e9b6ffd7a..cf6f54afa 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 68937d899..4043976aa 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 ea7b02b9a..c04732203 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 56158a469..af1e2ab3b 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 bee372275..a97ce6e95 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 1118e0771..9fbf324f0 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 eb05cef2e..009a347b8 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 61fcad7be..ef2da86c0 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 68937d899..94ebfcbcb 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 ea7b02b9a..552ca9203 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 fac78ee17..e70858ab0 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 1335b31de..e8bafbbab 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 cd0200f8d..96c56a18a 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 9fbf324f0..529257037 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 ff6413b67..42fca4504 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 36738cff8..73d30973c 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 bf1b3c6cd..ba7b9fdb1 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 47369498e..9235c4b54 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 5c74cb4d7..512c8c65f 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 70bd15d55..a7b47de34 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 f5c2381a0..5678fcba8 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 529257037..811d7724c 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 b2ac149d1..0cd36e33e 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 e70858ab0..4026e7e0a 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 e8bafbbab..ede668a6b 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 ab32eb9d6..a19c4e48f 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 75da900d8..af65f378a 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 96c56a18a..7df4951b2 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 08bf278b3..e9c8bc2b5 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 d5be5fa19..a9e52775b 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 db08cf476..bc73ce6ca 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 3e26f6e86..ca807f10d 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 7c98f1b8c..0522bc304 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 ae91044ae..bc3463842 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 e486cdf5f..95f96d8fc 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 a9e52775b..2938c6c72 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 6f7a22444..6e32bc710 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 eb5613c45..16d7c8f95 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 89a5b0917..aa66beaef 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 be2ba466f..8040cdfeb 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 98c8be9e0..bd6bb57c2 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 6e32bc710..6bbe34fc4 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 2c2c8efeb..4375edc5a 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 c46b8942e..06bebe6cb 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 5d265575c..e5d0c3edb 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 92f001805..bc1faec9f 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 2d01ef5a1..c7c3bf6ed 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 c306b73a7..06651d0f4 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 20ca8751f..5aeb8dec5 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.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.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 a128c44d1..2c1145003 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 b975f449c..2b7b23e7c 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 2725d3525..9bf4f4d1a 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.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 811d7724c..594dccfe3 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 18aa04c65..eefc27a7e 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 030377251..093b97064 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 @@ - +

{{ day.mday }}

diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 985f284c8..7b088b76f 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 | 78 ++++++++++++------- 9 files changed, 118 insertions(+), 64 deletions(-) diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 7b088b76f..269310608 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 74db1aebc..d56a3470b 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 cc930e728..c3b566f88 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 4cc40a194..b8d8ef9f4 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 d23579df0..8edc9b96e 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 341cf36b3..8f3d5a129 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 b395ae6af..bc9d96f33 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 17f372590..892cb35cd 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 36d1f6518..7fd222659 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; + treatedDays[dayId] = dayId; - promises.push(this.calendarProvider.invalidateDayEvents(day.year(), day.month() + 1, day.date(), - site.id)); + 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 ff2d77c97..6558abcb4 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 0a4b7bb1e..68f174608 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 9cfd3705a..34335a7de 100644 --- a/src/addon/calendar/pages/day/day.html +++ b/src/addon/calendar/pages/day/day.html @@ -48,7 +48,7 @@ - +

diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index 2d85b3f1d..3ae4f4c0d 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 | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/addon/calendar/components/calendar/calendar.scss b/src/addon/calendar/components/calendar/calendar.scss index 6558abcb4..b04fe3d64 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; + .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; + &.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 744ea653e..1ab1ad371 100644 --- a/config.xml +++ b/config.xml @@ -113,33 +113,33 @@ - - + + - - - - + + + + - - + + - - - - + + + + - - - - - - - - - + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index d08a2f82f..c82b49deb 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 dc9a19748..8d5fda2e1 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 4a2d344e2..6a629cb74 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 6a629cb74..931d8e4b5 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 0b7d683b8..30d79ac4b 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,