diff --git a/config.xml b/config.xml index 9948044e3..0946d1ed6 100644 --- a/config.xml +++ b/config.xml @@ -33,7 +33,7 @@ - + diff --git a/src/addon/calendar/calendar.module.ts b/src/addon/calendar/calendar.module.ts index 6269c08b5..f82454173 100644 --- a/src/addon/calendar/calendar.module.ts +++ b/src/addon/calendar/calendar.module.ts @@ -73,5 +73,8 @@ export class AddonCalendarModule { 'categoryid', 'groupid', 'userid', 'instance', 'modulename', 'timemodified', 'repeatid', 'visible', 'uuid', 'sequence', 'subscriptionid', 'notificationtime'] }); + + // Migrate the component name. + updateManager.registerLocalNotifComponentMigration('mmaCalendarComponent', AddonCalendarProvider.COMPONENT); } } diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 226fde369..794818ff7 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -31,9 +31,9 @@ export class AddonCalendarProvider { static DAYS_INTERVAL = 30; static COMPONENT = 'AddonCalendarEvents'; static DEFAULT_NOTIFICATION_TIME_CHANGED = 'AddonCalendarDefaultNotificationTimeChangedEvent'; - protected DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime'; + static DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime'; + static DEFAULT_NOTIFICATION_TIME = 60; protected ROOT_CACHE_KEY = 'mmaCalendar:'; - protected DEFAULT_NOTIFICATION_TIME = 60; // Variables for database. static EVENTS_TABLE = 'addon_calendar_events'; @@ -136,6 +136,18 @@ export class AddonCalendarProvider { this.sitesProvider.createTablesFromSchema(this.tablesSchema); } + /** + * Get all calendar events from local Db. + * + * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. + * @return {Promise} Promise resolved with all the events. + */ + getAllEventsFromLocalDb(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getAllRecords(AddonCalendarProvider.EVENTS_TABLE); + }); + } + /** * Get the configured default notification time. * @@ -145,9 +157,9 @@ export class AddonCalendarProvider { getDefaultNotificationTime(siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - const key = this.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId; + const key = AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId; - return this.configProvider.get(key, this.DEFAULT_NOTIFICATION_TIME); + return this.configProvider.get(key, AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME); } /** @@ -496,11 +508,24 @@ export class AddonCalendarProvider { setDefaultNotificationTime(time: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - const key = this.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId; + const key = AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId; return this.configProvider.set(key, time); } + /** + * Store an event in local DB as it is. + * + * @param {any} event Event to store. + * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. + * @return {Promise} Promise resolved when stored. + */ + storeEventInLocalDb(event: any, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().insertRecord(AddonCalendarProvider.EVENTS_TABLE, event); + }); + } + /** * Store events in local DB. * diff --git a/src/addon/messages/messages.module.ts b/src/addon/messages/messages.module.ts index db6c558fa..90f4abff9 100644 --- a/src/addon/messages/messages.module.ts +++ b/src/addon/messages/messages.module.ts @@ -131,5 +131,9 @@ export class AddonMessagesModule { } ] }); + + // Migrate the component name. + updateManager.registerLocalNotifComponentMigration('mmaMessagesPushSimulation', + AddonMessagesProvider.PUSH_SIMULATION_COMPONENT); } } diff --git a/src/addon/mod/glossary/pages/edit/edit.html b/src/addon/mod/glossary/pages/edit/edit.html index f42d75b3e..928efecdd 100644 --- a/src/addon/mod/glossary/pages/edit/edit.html +++ b/src/addon/mod/glossary/pages/edit/edit.html @@ -15,9 +15,7 @@ {{ 'addon.mod_glossary.definition' | translate }} - - + {{ 'addon.mod_glossary.categories' | translate }} 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 2bb0a5f6f..50c203f31 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 @@ -37,7 +37,7 @@ {{ 'addon.mod_lesson.enterpassword' | translate }} - + diff --git a/src/addon/mod/lesson/pages/password-modal/password-modal.html b/src/addon/mod/lesson/pages/password-modal/password-modal.html index dd55a244a..bde436b31 100644 --- a/src/addon/mod/lesson/pages/password-modal/password-modal.html +++ b/src/addon/mod/lesson/pages/password-modal/password-modal.html @@ -13,7 +13,7 @@ {{ 'addon.mod_lesson.enterpassword' | translate }} - + diff --git a/src/addon/mod/wiki/pages/edit/edit.html b/src/addon/mod/wiki/pages/edit/edit.html index b8b029fb2..4acb53dc9 100644 --- a/src/addon/mod/wiki/pages/edit/edit.html +++ b/src/addon/mod/wiki/pages/edit/edit.html @@ -20,7 +20,9 @@ - {{ 'addon.mod_wiki.wrongversionlock' | translate }} + + {{ 'addon.mod_wiki.wrongversionlock' | translate }} + diff --git a/src/addon/mod/wiki/pages/edit/edit.scss b/src/addon/mod/wiki/pages/edit/edit.scss new file mode 100644 index 000000000..ccb35ae13 --- /dev/null +++ b/src/addon/mod/wiki/pages/edit/edit.scss @@ -0,0 +1,5 @@ +page-addon-mod-wiki-edit { + .addon-mod_wiki-wrongversionlock .label { + margin: 0; + } +} diff --git a/src/addon/pushnotifications/pushnotifications.module.ts b/src/addon/pushnotifications/pushnotifications.module.ts index 78b9966b6..257aae6eb 100644 --- a/src/addon/pushnotifications/pushnotifications.module.ts +++ b/src/addon/pushnotifications/pushnotifications.module.ts @@ -83,5 +83,8 @@ export class AddonPushNotificationsModule { } ] }); + + // Migrate the component name. + updateManager.registerLocalNotifComponentMigration('mmaPushNotifications', AddonPushNotificationsProvider.COMPONENT); } } diff --git a/src/addon/userprofilefield/checkbox/component/addon-user-profile-field-checkbox.html b/src/addon/userprofilefield/checkbox/component/addon-user-profile-field-checkbox.html index 41170009e..086d5534f 100644 --- a/src/addon/userprofilefield/checkbox/component/addon-user-profile-field-checkbox.html +++ b/src/addon/userprofilefield/checkbox/component/addon-user-profile-field-checkbox.html @@ -10,8 +10,10 @@ - {{ field.name }} + + {{ field.name }} + + - \ No newline at end of file diff --git a/src/addon/userprofilefield/datetime/component/addon-user-profile-field-datetime.html b/src/addon/userprofilefield/datetime/component/addon-user-profile-field-datetime.html index 5f7a554a0..34423ce92 100644 --- a/src/addon/userprofilefield/datetime/component/addon-user-profile-field-datetime.html +++ b/src/addon/userprofilefield/datetime/component/addon-user-profile-field-datetime.html @@ -7,4 +7,5 @@ {{ field.name }} + \ No newline at end of file diff --git a/src/addon/userprofilefield/menu/component/addon-user-profile-field-menu.html b/src/addon/userprofilefield/menu/component/addon-user-profile-field-menu.html index ce81fe385..351812d26 100644 --- a/src/addon/userprofilefield/menu/component/addon-user-profile-field-menu.html +++ b/src/addon/userprofilefield/menu/component/addon-user-profile-field-menu.html @@ -10,4 +10,5 @@ {{ 'core.choosedots' | translate }} {{option}} + diff --git a/src/addon/userprofilefield/text/component/addon-user-profile-field-text.html b/src/addon/userprofilefield/text/component/addon-user-profile-field-text.html index 8701248f2..1ca1a727d 100644 --- a/src/addon/userprofilefield/text/component/addon-user-profile-field-text.html +++ b/src/addon/userprofilefield/text/component/addon-user-profile-field-text.html @@ -7,4 +7,5 @@ {{ field.name }} + diff --git a/src/addon/userprofilefield/textarea/component/addon-user-profile-field-textarea.html b/src/addon/userprofilefield/textarea/component/addon-user-profile-field-textarea.html index 4921c3675..4a993015b 100644 --- a/src/addon/userprofilefield/textarea/component/addon-user-profile-field-textarea.html +++ b/src/addon/userprofilefield/textarea/component/addon-user-profile-field-textarea.html @@ -5,6 +5,9 @@ - {{ field.name }} + + {{ field.name }} + + \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 5af93f0fe..d93ed1754 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -15,7 +15,6 @@ import { Component, OnInit } from '@angular/core'; import { Platform } from 'ionic-angular'; import { StatusBar } from '@ionic-native/status-bar'; -import { SplashScreen } from '@ionic-native/splash-screen'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreLangProvider } from '@providers/lang'; @@ -33,7 +32,7 @@ export class MoodleMobileApp implements OnInit { protected logger; protected lastUrls = {}; - constructor(private platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen, logger: CoreLoggerProvider, + constructor(private platform: Platform, statusBar: StatusBar, logger: CoreLoggerProvider, private eventsProvider: CoreEventsProvider, private loginHelper: CoreLoginHelperProvider, private appProvider: CoreAppProvider, private langProvider: CoreLangProvider, private sitesProvider: CoreSitesProvider) { this.logger = logger.getInstance('AppComponent'); @@ -46,8 +45,6 @@ export class MoodleMobileApp implements OnInit { } else { statusBar.styleDefault(); } - - splashScreen.hide(); }); } diff --git a/src/classes/site.ts b/src/classes/site.ts index 9f380aa46..ce3693047 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -1226,13 +1226,14 @@ export class CoreSite { } if (alertMessage) { - const alert = this.domUtils.showAlert(this.translate.instant('core.notice'), alertMessage, undefined, 3000); - alert.onDidDismiss(() => { - if (inApp) { - resolve(this.utils.openInApp(url, options)); - } else { - resolve(this.utils.openInBrowser(url)); - } + this.domUtils.showAlert(this.translate.instant('core.notice'), alertMessage, undefined, 3000).then((alert) => { + alert.onDidDismiss(() => { + if (inApp) { + resolve(this.utils.openInApp(url, options)); + } else { + resolve(this.utils.openInBrowser(url)); + } + }); }); } else { if (inApp) { diff --git a/src/components/input-errors/core-input-errors.html b/src/components/input-errors/core-input-errors.html index 390ad0ce0..e960807e8 100644 --- a/src/components/input-errors/core-input-errors.html +++ b/src/components/input-errors/core-input-errors.html @@ -1,7 +1,15 @@ - + diff --git a/src/core/login/pages/init/init.ts b/src/core/login/pages/init/init.ts index 2770bcf4e..9d58eb521 100644 --- a/src/core/login/pages/init/init.ts +++ b/src/core/login/pages/init/init.ts @@ -14,6 +14,7 @@ import { Component } from '@angular/core'; import { IonicPage, NavController } from 'ionic-angular'; +import { SplashScreen } from '@ionic-native/splash-screen'; import { CoreAppProvider } from '@providers/app'; import { CoreInitDelegate } from '@providers/init'; import { CoreSitesProvider } from '@providers/sites'; @@ -31,7 +32,8 @@ import { CoreLoginHelperProvider } from '../../providers/helper'; export class CoreLoginInitPage { constructor(private navCtrl: NavController, private appProvider: CoreAppProvider, private initDelegate: CoreInitDelegate, - private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider) { } + private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider, + private splashScreen: SplashScreen) { } /** * View loaded. @@ -51,11 +53,11 @@ export class CoreLoginInitPage { // The redirect is pointing to a site, load it. return this.sitesProvider.loadSite(redirectData.siteId).then(() => { if (!this.loginHelper.isSiteLoggedOut(redirectData.page, redirectData.params)) { - this.navCtrl.setRoot(redirectData.page, redirectData.params, { animate: false }); + return this.navCtrl.setRoot(redirectData.page, redirectData.params, { animate: false }); } }).catch(() => { // Site doesn't exist. - this.loadPage(); + return this.loadPage(); }); } else { // No site to load, just open the state. @@ -64,24 +66,37 @@ export class CoreLoginInitPage { } } - this.loadPage(); + return this.loadPage(); + }).then(() => { + // If we hide the splash screen now, the init view is still seen for an instant. Wait a bit to make sure it isn't seen. + setTimeout(() => { + this.splashScreen.hide(); + }, 100); }); } /** * Load the right page. + * + * @return {Promise} Promise resolved when done. */ - protected loadPage(): void { + protected loadPage(): Promise { if (this.sitesProvider.isLoggedIn()) { if (!this.loginHelper.isSiteLoggedOut()) { - this.loginHelper.goToSiteInitialPage(); + // User is logged in, go to site initial page. + return this.loginHelper.goToSiteInitialPage(); + } else { + // The site is marked as logged out. Logout and try again. + return this.sitesProvider.logout().then(() => { + return this.loadPage(); + }); } } else { - this.sitesProvider.hasSites().then((hasSites) => { + return this.sitesProvider.hasSites().then((hasSites) => { if (hasSites) { - this.navCtrl.setRoot('CoreLoginSitesPage'); + return this.navCtrl.setRoot('CoreLoginSitesPage'); } else { - this.loginHelper.goToAddSite(true); + return this.loginHelper.goToAddSite(true); } }); } diff --git a/src/core/login/pages/reconnect/reconnect.html b/src/core/login/pages/reconnect/reconnect.html index f2b454762..47bfdf501 100644 --- a/src/core/login/pages/reconnect/reconnect.html +++ b/src/core/login/pages/reconnect/reconnect.html @@ -33,7 +33,7 @@
- + diff --git a/src/core/login/pages/site/site.html b/src/core/login/pages/site/site.html index 6416bedce..a07eebbbd 100644 --- a/src/core/login/pages/site/site.html +++ b/src/core/login/pages/site/site.html @@ -19,7 +19,7 @@

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

- +
diff --git a/src/core/login/pages/site/site.ts b/src/core/login/pages/site/site.ts index 77bb3eb2c..dcd6752fb 100644 --- a/src/core/login/pages/site/site.ts +++ b/src/core/login/pages/site/site.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component } from '@angular/core'; -import { IonicPage, NavController, ModalController } from 'ionic-angular'; +import { IonicPage, NavController, ModalController, NavParams } from 'ionic-angular'; import { CoreAppProvider } from '@providers/app'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; @@ -33,10 +33,14 @@ export class CoreLoginSitePage { siteForm: FormGroup; fixedSites: any[]; displayAsButtons = false; + showKeyboard = false; - constructor(private navCtrl: NavController, fb: FormBuilder, private appProvider: CoreAppProvider, + constructor(navParams: NavParams, private navCtrl: NavController, fb: FormBuilder, private appProvider: CoreAppProvider, private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider, private modalCtrl: ModalController, private domUtils: CoreDomUtilsProvider) { + + this.showKeyboard = !!navParams.get('showKeyboard'); + let url = ''; // Load fixed sites if they're set. diff --git a/src/core/login/pages/sites/sites.ts b/src/core/login/pages/sites/sites.ts index 7e659e9c6..a1706e522 100644 --- a/src/core/login/pages/sites/sites.ts +++ b/src/core/login/pages/sites/sites.ts @@ -67,7 +67,7 @@ export class CoreLoginSitesPage { * Go to the page to add a site. */ add(): void { - this.loginHelper.goToAddSite(false); + this.loginHelper.goToAddSite(false, true); } /** @@ -91,7 +91,7 @@ export class CoreLoginSitesPage { // If there are no sites left, go to add site. this.sitesProvider.hasSites().then((hasSites) => { if (!hasSites) { - this.loginHelper.goToAddSite(true); + this.loginHelper.goToAddSite(true, true); } }); }).catch((error) => { diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index e0cfbb6ec..942fb37da 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -380,9 +380,10 @@ export class CoreLoginHelperProvider { * If a fixed URL is configured, go to credentials instead. * * @param {boolean} [setRoot] True to set the new page as root, false to add it to the stack. + * @param {boolean} [showKeyboard] Whether to show keyboard in the new page. Only if no fixed URL set. * @return {Promise} Promise resolved when done. */ - goToAddSite(setRoot?: boolean): Promise { + goToAddSite(setRoot?: boolean, showKeyboard?: boolean): Promise { let pageName, params; @@ -395,6 +396,9 @@ export class CoreLoginHelperProvider { params = { siteUrl: url }; } else { pageName = 'CoreLoginSitePage'; + params = { + showKeyboard: showKeyboard + }; } if (setRoot) { @@ -685,9 +689,10 @@ export class CoreLoginHelperProvider { * @param {string} error Error message. */ openChangePassword(siteUrl: string, error: string): void { - const alert = this.domUtils.showAlert(this.translate.instant('core.notice'), error, undefined, 3000); - alert.onDidDismiss(() => { - this.utils.openInApp(siteUrl + '/login/change_password.php'); + this.domUtils.showAlert(this.translate.instant('core.notice'), error, undefined, 3000).then((alert) => { + alert.onDidDismiss(() => { + this.utils.openInApp(siteUrl + '/login/change_password.php'); + }); }); } diff --git a/src/core/siteplugins/components/course-format/core-siteplugins-course-format.html b/src/core/siteplugins/components/course-format/core-siteplugins-course-format.html index cd6008506..db841ebb6 100644 --- a/src/core/siteplugins/components/course-format/core-siteplugins-course-format.html +++ b/src/core/siteplugins/components/course-format/core-siteplugins-course-format.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/core/siteplugins/components/course-format/course-format.ts b/src/core/siteplugins/components/course-format/course-format.ts index 87b37a4f0..e1bf8de9e 100644 --- a/src/core/siteplugins/components/course-format/course-format.ts +++ b/src/core/siteplugins/components/course-format/course-format.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, Input, ViewChild } from '@angular/core'; +import { Component, OnChanges, Input, ViewChild } from '@angular/core'; import { CoreSitePluginsProvider } from '../../providers/siteplugins'; import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin-content'; @@ -23,10 +23,12 @@ import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin- selector: 'core-site-plugins-course-format', templateUrl: 'core-siteplugins-course-format.html', }) -export class CoreSitePluginsCourseFormatComponent implements OnInit { +export class CoreSitePluginsCourseFormatComponent implements OnChanges { @Input() course: any; // The course to render. @Input() sections: any[]; // List of course sections. @Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled. + @Input() initialSectionId?: number; // The section to load first (by ID). + @Input() initialSectionNumber?: number; // The section to load first (by number). @ViewChild(CoreSitePluginsPluginContentComponent) content: CoreSitePluginsPluginContentComponent; @@ -34,24 +36,37 @@ export class CoreSitePluginsCourseFormatComponent implements OnInit { method: string; args: any; initResult: any; + data: any; constructor(protected sitePluginsProvider: CoreSitePluginsProvider) { } /** - * Component being initialized. + * Detect changes on input properties. */ - ngOnInit(): void { + ngOnChanges(): void { if (this.course && this.course.format) { - const handler = this.sitePluginsProvider.getSitePluginHandler(this.course.format); - if (handler) { - this.component = handler.plugin.component; - this.method = handler.handlerSchema.method; - this.args = { - courseid: this.course.id, - downloadenabled: this.downloadEnabled - }; - this.initResult = handler.initResult; + if (!this.component) { + // Initialize the data. + const handler = this.sitePluginsProvider.getSitePluginHandler(this.course.format); + if (handler) { + this.component = handler.plugin.component; + this.method = handler.handlerSchema.method; + this.args = { + courseid: this.course.id, + downloadenabled: this.downloadEnabled + }; + this.initResult = handler.initResult; + } } + + // Pass input data to the component. + this.data = { + course: this.course, + sections: this.sections, + downloadEnabled: this.downloadEnabled, + initialSectionId: this.initialSectionId, + initialSectionNumber: this.initialSectionNumber + }; } } diff --git a/src/core/siteplugins/components/plugin-content/plugin-content.ts b/src/core/siteplugins/components/plugin-content/plugin-content.ts index 21b9f8c2f..60f8eca1e 100644 --- a/src/core/siteplugins/components/plugin-content/plugin-content.ts +++ b/src/core/siteplugins/components/plugin-content/plugin-content.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, Input, Output, EventEmitter, Optional } from '@angular/core'; +import { Component, OnInit, Input, Output, EventEmitter, Optional, DoCheck, KeyValueDiffers } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreSitePluginsProvider } from '../../providers/siteplugins'; @@ -25,11 +25,12 @@ import { Subject } from 'rxjs'; selector: 'core-site-plugins-plugin-content', templateUrl: 'core-siteplugins-plugin-content.html', }) -export class CoreSitePluginsPluginContentComponent implements OnInit { +export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck { @Input() component: string; @Input() method: string; @Input() args: any; @Input() initResult: any; // Result of the init WS call of the handler. + @Input() data: any; // Data to pass to the component. @Output() onContentLoaded?: EventEmitter; // Emits an event when the content is loaded. @Output() onLoadingContent?: EventEmitter; // Emits an event when starts to load the content. @@ -40,11 +41,14 @@ export class CoreSitePluginsPluginContentComponent implements OnInit { invalidateObservable: Subject; // An observable to notify observers when to invalidate data. jsData: any; // Data to pass to the component. + protected differ: any; // To detect changes in the data input. + constructor(protected domUtils: CoreDomUtilsProvider, protected sitePluginsProvider: CoreSitePluginsProvider, - @Optional() protected navCtrl: NavController) { + @Optional() protected navCtrl: NavController, differs: KeyValueDiffers) { this.onContentLoaded = new EventEmitter(); this.onLoadingContent = new EventEmitter(); this.invalidateObservable = new Subject(); + this.differ = differs.find([]).create(); } /** @@ -54,6 +58,21 @@ export class CoreSitePluginsPluginContentComponent implements OnInit { this.fetchContent(); } + /** + * Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays). + */ + ngDoCheck(): void { + if (!this.data || !this.jsData) { + return; + } + + // Check if there's any change in the data object. + const changes = this.differ.diff(this.data); + if (changes) { + this.jsData = Object.assign(this.jsData, this.data); + } + } + /** * Fetches the content to render. * @@ -67,7 +86,9 @@ export class CoreSitePluginsPluginContentComponent implements OnInit { this.content = result.templates.length ? result.templates[0].html : ''; // Load first template. this.javascript = result.javascript; this.otherData = result.otherdata; - this.jsData = this.sitePluginsProvider.createDataForJS(this.initResult, result); + this.data = this.data || {}; + + this.jsData = Object.assign(this.data, this.sitePluginsProvider.createDataForJS(this.initResult, result)); // Pass some methods as jsData so they can be called from the template too. this.jsData.openContent = this.openContent.bind(this); diff --git a/src/directives/external-content.ts b/src/directives/external-content.ts index e16187522..93a20ee76 100644 --- a/src/directives/external-content.ts +++ b/src/directives/external-content.ts @@ -187,7 +187,6 @@ export class CoreExternalContentDirective implements AfterViewInit { this.logger.debug('Using URL ' + finalUrl + ' for ' + url); if (tagName === 'SOURCE') { // The browser does not catch changes in SRC, we need to add a new source. - // @todo: Check if changing src works in Android 4.4, maybe the problem was only in 4.1-4.3. this.addSource(finalUrl); } else { this.element.setAttribute(targetAttr, finalUrl); diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index 48cfb470f..d8cc92fd5 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -186,7 +186,6 @@ export class CoreFormatTextDirective implements OnChanges { this.element.innerHTML = ''; // Remove current contents. if (this.maxHeight && div.innerHTML != '') { // Move the children to the current element to be able to calculate the height. - // @todo: Display the element? this.domUtils.moveChildren(div, this.element); // Height cannot be calculated if the element is not shown while calculating. diff --git a/src/directives/link.ts b/src/directives/link.ts index 7165f82f7..1b5700efb 100644 --- a/src/directives/link.ts +++ b/src/directives/link.ts @@ -90,8 +90,8 @@ export class CoreLinkDirective implements OnInit { href = href.substr(1); // In site links if (href.charAt(0) == '/') { - // @todo: Investigate how to achieve this behaviour. - // $location.url(href); + // @todo: This cannot be achieved with push/pop navigation, location.go() doesn't update the state, only the URL. + // In Ionic 4 the navigation will change, so maybe it can be done by then. } else { // Look for id or name. this.domUtils.scrollToElementBySelector(this.content, '#' + href + ', [name=\'' + href + '\']'); diff --git a/src/providers/local-notifications.ts b/src/providers/local-notifications.ts index 4d0541514..41d2d18ee 100644 --- a/src/providers/local-notifications.ts +++ b/src/providers/local-notifications.ts @@ -47,9 +47,6 @@ export interface CoreILocalNotification extends ILocalNotification { * * See https://angular.io/guide/dependency-injection for more info on providers * and Angular DI. - * - * @todo We might have to translate the old component name to the new one. - * Otherwise the unique ID of local notifications could change. */ @Injectable() export class CoreLocalNotificationsProvider { @@ -502,4 +499,18 @@ export class CoreLocalNotificationsProvider { return this.appDB.insertRecord(this.TRIGGERED_TABLE, entry); } + + /** + * Update a component name. + * + * @param {string} oldName The old name. + * @param {string} newName The new name. + * @return {Promise} Promise resolved when done. + */ + updateComponentName(oldName: string, newName: string): Promise { + const oldId = this.COMPONENTS_TABLE + '#' + oldName, + newId = this.COMPONENTS_TABLE + '#' + newName; + + return this.appDB.updateRecords(this.COMPONENTS_TABLE, {id: newId}, {id: oldId}); + } } diff --git a/src/providers/update-manager.ts b/src/providers/update-manager.ts index 183d1b32b..744adcd5b 100644 --- a/src/providers/update-manager.ts +++ b/src/providers/update-manager.ts @@ -22,6 +22,7 @@ import { CoreLoggerProvider } from './logger'; import { CoreSitesProvider } from './sites'; import { CoreUtilsProvider } from './utils/utils'; import { CoreConfigConstants } from '../configconstants'; +import { AddonCalendarProvider } from '@addon/calendar/providers/calendar'; import { SQLiteDB } from '@classes/sqlitedb'; /** @@ -71,6 +72,7 @@ export class CoreUpdateManagerProvider implements CoreInitHandler { protected VERSION_APPLIED = 'version_applied'; protected logger; + protected localNotificationsComponentsMigrate: {[old: string]: string} = {}; /** * Tables to migrate from app DB ('MoodleMobile'). Include all the core ones to decrease the dependencies. @@ -323,7 +325,8 @@ export class CoreUpdateManagerProvider implements CoreInitHandler { constructor(logger: CoreLoggerProvider, private configProvider: CoreConfigProvider, private sitesProvider: CoreSitesProvider, private filepoolProvider: CoreFilepoolProvider, private notifProvider: CoreLocalNotificationsProvider, - private utils: CoreUtilsProvider, private appProvider: CoreAppProvider) { + private utils: CoreUtilsProvider, private appProvider: CoreAppProvider, + private calendarProvider: AddonCalendarProvider) { this.logger = logger.getInstance('CoreUpdateManagerProvider'); } @@ -341,6 +344,9 @@ export class CoreUpdateManagerProvider implements CoreInitHandler { if (!versionApplied) { // No version applied, either the app was just installed or it's being updated from Ionic 1. return this.migrateAllDBs().then(() => { + // Now that the DBs have been migrated, migrate the local notification components names. + return this.migrateLocalNotificationsComponents(); + }).then(() => { // DBs migrated, get the version applied again. return this.configProvider.get(this.VERSION_APPLIED, 0); }); @@ -358,9 +364,8 @@ export class CoreUpdateManagerProvider implements CoreInitHandler { promises.push(this.setSitesConfig()); } - if (versionCode >= 2018 && versionApplied < 2018 && versionApplied > 0) { - promises.push(this.adaptForumOfflineStores()); - } + // In version 2018 we adapted the forum offline stores to match a new schema. + // However, due to the migration of data to SQLite we can no longer do that. return Promise.all(promises).then(() => { return this.configProvider.set(this.VERSION_APPLIED, versionCode); @@ -410,6 +415,16 @@ export class CoreUpdateManagerProvider implements CoreInitHandler { this.siteDBTables.push(table); } + /** + * Register a migration of component name for local notifications. + * + * @param {string} oldName The old name. + * @param {string} newName The new name. + */ + registerLocalNotifComponentMigration(oldName: string, newName: string): void { + this.localNotificationsComponentsMigrate[oldName] = newName; + } + /** * Migrate all DBs and tables from the old format to SQLite. * @@ -555,6 +570,30 @@ export class CoreUpdateManagerProvider implements CoreInitHandler { }); } + /** + * Migrate local notifications components from the old nomenclature to the new one. + * + * @return {Promise} Promise resolved when done. + */ + protected migrateLocalNotificationsComponents(): Promise { + if (!this.notifProvider.isAvailable()) { + // Local notifications not available, nothing to do. + return Promise.resolve(); + } + + const promises = []; + + for (const oldName in this.localNotificationsComponentsMigrate) { + const newName = this.localNotificationsComponentsMigrate[oldName]; + + promises.push(this.notifProvider.updateComponentName(oldName, newName).catch((error) => { + this.logger.error('Error migrating local notif component from ' + oldName + ' to ' + newName + ': ', error); + })); + } + + return Promise.all(promises); + } + /** * Calendar default notification time is configurable from version 3.2.1, and a new option "Default" is added. * All events that were configured to use the fixed default time should now be configured to use "Default" option. @@ -567,8 +606,27 @@ export class CoreUpdateManagerProvider implements CoreInitHandler { return Promise.resolve(); } - // @todo: Implement it once Calendar addon is implemented. - return Promise.resolve(); + return this.sitesProvider.getSitesIds().then((siteIds) => { + + const promises = []; + siteIds.forEach((siteId) => { + // Get stored events. + promises.push(this.calendarProvider.getAllEventsFromLocalDb(siteId).then((events) => { + const eventPromises = []; + + events.forEach((event) => { + if (event.notificationtime == AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME) { + event.notificationtime = -1; + eventPromises.push(this.calendarProvider.storeEventInLocalDb(event, siteId)); + } + }); + + return Promise.all(eventPromises); + })); + }); + + return Promise.all(promises); + }); } /** @@ -626,15 +684,4 @@ export class CoreUpdateManagerProvider implements CoreInitHandler { }); }); } - - /** - * The data stored for offline discussions and posts changed its format. Adapt the entries already stored. - * Since it can be slow, we'll only block migrating the db of current site, the rest will be in background. - * - * @return {Promise} Promise resolved when the db is migrated. - */ - protected adaptForumOfflineStores(): Promise { - // @todo: Implement it once Forum addon is implemented. - return Promise.resolve(); - } } diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index a7b961e9b..8a21c45ee 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -44,22 +44,6 @@ export class CoreDomUtilsProvider { private platform: Platform, private configProvider: CoreConfigProvider, private urlUtils: CoreUrlUtilsProvider, private modalCtrl: ModalController) { } - /** - * Wraps a message with core-format-text if the message contains HTML tags. - * @todo Finish the adaptation - * - * @param {string} message Message to wrap. - * @return {string} Result message. - */ - private addFormatTextIfNeeded(message: string): string { - // @todo - if (this.textUtils.hasHTMLTags(message)) { - return '' + message + ''; - } - - return message; - } - /** * Equivalent to element.closest(). If the browser doesn't support element.closest, it will * traverse the parents to achieve the same functionality. @@ -776,24 +760,43 @@ export class CoreDomUtilsProvider { * @param {string} message Message to show. * @param {string} [buttonText] Text of the button. * @param {number} [autocloseTime] Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. - * @return {Alert} The alert modal. + * @return {Promise} Promise resolved with the alert modal. */ - showAlert(title: string, message: string, buttonText?: string, autocloseTime?: number): Alert { - const alert = this.alertCtrl.create({ - title: title, - message: this.addFormatTextIfNeeded(message), // Add format-text to handle links. - buttons: [buttonText || this.translate.instant('core.ok')] - }); + showAlert(title: string, message: string, buttonText?: string, autocloseTime?: number): Promise { + const hasHTMLTags = this.textUtils.hasHTMLTags(message); + let promise; - alert.present(); - - if (autocloseTime > 0) { - setTimeout(() => { - alert.dismiss(); - }, autocloseTime); + if (hasHTMLTags) { + // Format the text. + promise = this.textUtils.formatText(message); + } else { + promise = Promise.resolve(message); } - return alert; + return promise.then((message) => { + + const alert = this.alertCtrl.create({ + title: title, + message: message, + buttons: [buttonText || this.translate.instant('core.ok')] + }); + + alert.present().then(() => { + if (hasHTMLTags) { + // Treat all anchors so they don't override the app. + const alertMessageEl: HTMLElement = alert.pageRef().nativeElement.querySelector('.alert-message'); + this.treatAnchors(alertMessageEl); + } + }); + + if (autocloseTime > 0) { + setTimeout(() => { + alert.dismiss(); + }, autocloseTime); + } + + return alert; + }); } /** @@ -803,9 +806,9 @@ export class CoreDomUtilsProvider { * @param {string} message Message to show. * @param {string} [buttonText] Text of the button. * @param {number} [autocloseTime] Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. - * @return {Alert} The alert modal. + * @return {Promise} Promise resolved with the alert modal. */ - showAlertTranslated(title: string, message: string, buttonText?: string, autocloseTime?: number): Alert { + showAlertTranslated(title: string, message: string, buttonText?: string, autocloseTime?: number): Promise { title = title ? this.translate.instant(title) : title; message = message ? this.translate.instant(message) : message; buttonText = buttonText ? this.translate.instant(buttonText) : buttonText; @@ -825,30 +828,50 @@ export class CoreDomUtilsProvider { */ showConfirm(message: string, title?: string, okText?: string, cancelText?: string, options?: any): Promise { return new Promise((resolve, reject): void => { - options = options || {}; + const hasHTMLTags = this.textUtils.hasHTMLTags(message); + let promise; - options.message = this.addFormatTextIfNeeded(message); // Add format-text to handle links. - options.title = title; - if (!title) { - options.cssClass = 'core-nohead'; + if (hasHTMLTags) { + // Format the text. + promise = this.textUtils.formatText(message); + } else { + promise = Promise.resolve(message); } - options.buttons = [ - { - text: cancelText || this.translate.instant('core.cancel'), - role: 'cancel', - handler: (): void => { - reject(this.createCanceledError()); - } - }, - { - text: okText || this.translate.instant('core.ok'), - handler: (): void => { - resolve(); - } - } - ]; - this.alertCtrl.create(options).present(); + promise.then((message) => { + options = options || {}; + + options.message = message; + options.title = title; + if (!title) { + options.cssClass = 'core-nohead'; + } + options.buttons = [ + { + text: cancelText || this.translate.instant('core.cancel'), + role: 'cancel', + handler: (): void => { + reject(this.createCanceledError()); + } + }, + { + text: okText || this.translate.instant('core.ok'), + handler: (): void => { + resolve(); + } + } + ]; + + const alert = this.alertCtrl.create(options); + + alert.present().then(() => { + if (hasHTMLTags) { + // Treat all anchors so they don't override the app. + const alertMessageEl: HTMLElement = alert.pageRef().nativeElement.querySelector('.alert-message'); + this.treatAnchors(alertMessageEl); + } + }); + }); }); } @@ -858,9 +881,9 @@ export class CoreDomUtilsProvider { * @param {any} error Message to show. * @param {boolean} [needsTranslate] Whether the error needs to be translated. * @param {number} [autocloseTime] Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. - * @return {Alert} The alert modal. + * @return {Promise} Promise resolved with the alert modal. */ - showErrorModal(error: any, needsTranslate?: boolean, autocloseTime?: number): Alert { + showErrorModal(error: any, needsTranslate?: boolean, autocloseTime?: number): Promise { if (typeof error == 'object') { // We received an object instead of a string. Search for common properties. if (error.coreCanceled) { @@ -903,9 +926,9 @@ export class CoreDomUtilsProvider { * @param {any} [defaultError] Message to show if the error is not a string. * @param {boolean} [needsTranslate] Whether the error needs to be translated. * @param {number} [autocloseTime] Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. - * @return {Alert} The alert modal. + * @return {Promise} Promise resolved with the alert modal. */ - showErrorModalDefault(error: any, defaultError: any, needsTranslate?: boolean, autocloseTime?: number): Alert { + showErrorModalDefault(error: any, defaultError: any, needsTranslate?: boolean, autocloseTime?: number): Promise { if (error && error.coreCanceled) { // It's a canceled error, don't display an error. return; @@ -927,9 +950,9 @@ export class CoreDomUtilsProvider { * @param {any} [defaultError] Message to show if the error is not a string. * @param {boolean} [needsTranslate] Whether the error needs to be translated. * @param {number} [autocloseTime] Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. - * @return {Alert} The alert modal. + * @return {Promise} Promise resolved with the alert modal. */ - showErrorModalFirstWarning(warnings: any, defaultError: any, needsTranslate?: boolean, autocloseTime?: number): Alert { + showErrorModalFirstWarning(warnings: any, defaultError: any, needsTranslate?: boolean, autocloseTime?: number): Promise { const error = warnings && warnings.length && warnings[0].message; return this.showErrorModalDefault(error, defaultError, needsTranslate, autocloseTime); @@ -974,32 +997,52 @@ export class CoreDomUtilsProvider { */ showPrompt(message: string, title?: string, placeholder?: string, type: string = 'password'): Promise { return new Promise((resolve, reject): void => { - this.alertCtrl.create({ - message: this.addFormatTextIfNeeded(message), // Add format-text to handle links. - title: title, - inputs: [ - { - name: 'promptinput', - placeholder: placeholder || this.translate.instant('core.login.password'), - type: type - } - ], - buttons: [ - { - text: this.translate.instant('core.cancel'), - role: 'cancel', - handler: (): void => { - reject(); + const hasHTMLTags = this.textUtils.hasHTMLTags(message); + let promise; + + if (hasHTMLTags) { + // Format the text. + promise = this.textUtils.formatText(message); + } else { + promise = Promise.resolve(message); + } + + promise.then((message) => { + const alert = this.alertCtrl.create({ + message: message, + title: title, + inputs: [ + { + name: 'promptinput', + placeholder: placeholder || this.translate.instant('core.login.password'), + type: type } - }, - { - text: this.translate.instant('core.ok'), - handler: (data): void => { - resolve(data.promptinput); + ], + buttons: [ + { + text: this.translate.instant('core.cancel'), + role: 'cancel', + handler: (): void => { + reject(); + } + }, + { + text: this.translate.instant('core.ok'), + handler: (data): void => { + resolve(data.promptinput); + } } + ] + }); + + alert.present().then(() => { + if (hasHTMLTags) { + // Treat all anchors so they don't override the app. + const alertMessageEl: HTMLElement = alert.pageRef().nativeElement.querySelector('.alert-message'); + this.treatAnchors(alertMessageEl); } - ] - }).present(); + }); + }); }); } @@ -1069,6 +1112,42 @@ export class CoreDomUtilsProvider { return element.children; } + /** + * Treat anchors inside alert/modals. + * + * @param {HTMLElement} container The HTMLElement that can contain anchors. + */ + protected treatAnchors(container: HTMLElement): void { + const anchors = Array.from(container.querySelectorAll('a')); + + anchors.forEach((anchor) => { + anchor.addEventListener('click', (event) => { + if (event.defaultPrevented) { + // Stop. + return; + } + + const href = anchor.getAttribute('href'); + if (href) { + event.preventDefault(); + event.stopPropagation(); + + // We cannot use CoreDomUtilsProvider.openInBrowser due to circular dependencies. + if (this.appProvider.isDesktop()) { + // It's a desktop app, use Electron shell library to open the browser. + const shell = require('electron').shell; + if (!shell.openExternal(href)) { + // Open browser failed, open a new window in the app. + window.open(href, '_system'); + } + } else { + window.open(href, '_system'); + } + } + }); + }); + } + /** * View an image in a new page or modal. *