diff --git a/config/webpack.config.js b/config/webpack.config.js new file mode 100644 index 000000000..2118f9cb2 --- /dev/null +++ b/config/webpack.config.js @@ -0,0 +1,22 @@ +const { resolve } = require('path'); +const webpackMerge = require('webpack-merge'); +const { dev, prod } = require('@ionic/app-scripts/config/webpack.config'); + +const customConfig = { + resolve: { + alias: { + '@addon': resolve('./src/addon'), + '@classes': resolve('./src/classes'), + '@core': resolve('./src/core'), + '@providers': resolve('./src/providers'), + '@components': resolve('./src/components'), + '@directives': resolve('./src/directives/directives.module'), + '@pipes': resolve('./src/pipes/pipes.module') + } + } +}; + +module.exports = { + dev: webpackMerge(dev, customConfig), + prod: webpackMerge(prod, customConfig), +} diff --git a/package.json b/package.json index 66725cfa5..8154175bd 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "name": "Moodle Pty Ltd.", "email": "mobile@moodle.com" }, + "config": { + "ionic_webpack": "./config/webpack.config.js" + }, "repository": { "type": "git", "url": "https://github.com/moodlehq/moodlemobile2.git" @@ -26,6 +29,7 @@ "ionic:build:before": "gulp" }, "dependencies": { + "@angular/animations": "^5.2.5", "@angular/common": "5.0.0", "@angular/compiler": "5.0.0", "@angular/compiler-cli": "5.0.0", @@ -34,9 +38,11 @@ "@angular/http": "5.0.0", "@angular/platform-browser": "5.0.0", "@angular/platform-browser-dynamic": "5.0.0", + "@ionic-native/badge": "^4.5.3", "@ionic-native/camera": "^4.5.2", "@ionic-native/clipboard": "^4.3.2", "@ionic-native/core": "4.3.0", + "@ionic-native/device": "^4.5.3", "@ionic-native/file": "^4.3.3", "@ionic-native/file-transfer": "^4.3.3", "@ionic-native/globalization": "^4.3.2", @@ -45,6 +51,7 @@ "@ionic-native/local-notifications": "^4.4.0", "@ionic-native/media-capture": "^4.5.2", "@ionic-native/network": "^4.3.2", + "@ionic-native/push": "^4.5.3", "@ionic-native/splash-screen": "4.3.0", "@ionic-native/sqlite": "^4.3.2", "@ionic-native/status-bar": "4.3.0", @@ -76,7 +83,8 @@ "gulp-rename": "^1.2.2", "gulp-slash": "^1.1.3", "through": "^2.3.8", - "typescript": "2.4.2" + "typescript": "2.4.2", + "webpack-merge": "^4.1.2" }, "browser": { "electron": false diff --git a/src/addon/calendar/calendar.module.ts b/src/addon/calendar/calendar.module.ts index 3fcfdf52b..1f4bea922 100644 --- a/src/addon/calendar/calendar.module.ts +++ b/src/addon/calendar/calendar.module.ts @@ -16,10 +16,10 @@ import { NgModule } from '@angular/core'; import { AddonCalendarProvider } from './providers/calendar'; import { AddonCalendarHelperProvider } from './providers/helper'; import { AddonCalendarMainMenuHandler } from './providers/mainmenu-handler'; -import { CoreMainMenuDelegate } from '../../core/mainmenu/providers/delegate'; -import { CoreInitDelegate } from '../../providers/init'; -import { CoreLocalNotificationsProvider } from '../../providers/local-notifications'; -import { CoreLoginHelperProvider } from '../../core/login/providers/helper'; +import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; +import { CoreInitDelegate } from '@providers/init'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { CoreLoginHelperProvider } from '@core/login/providers/helper'; @NgModule({ declarations: [ diff --git a/src/addon/calendar/pages/event/event.module.ts b/src/addon/calendar/pages/event/event.module.ts index 7731c3b2d..d579114cc 100644 --- a/src/addon/calendar/pages/event/event.module.ts +++ b/src/addon/calendar/pages/event/event.module.ts @@ -15,9 +15,9 @@ 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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; +import { CorePipesModule } from '@pipes'; import { AddonCalendarEventPage } from './event'; @NgModule({ diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index 85dfd003a..0be9e4935 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -17,11 +17,11 @@ import { IonicPage, Content, NavParams } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarHelperProvider } from '../../providers/helper'; -import { CoreCoursesProvider } from '../../../../core/courses/providers/courses'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; -import { CoreSitesProvider } from '../../../../providers/sites'; -import { CoreLocalNotificationsProvider } from '../../../../providers/local-notifications'; -import { CoreCourseProvider } from '../../../../core/course/providers/course'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { CoreCourseProvider } from '@core/course/providers/course'; import * as moment from 'moment'; /** diff --git a/src/addon/calendar/pages/list/list.module.ts b/src/addon/calendar/pages/list/list.module.ts index 9335a8061..b685b6578 100644 --- a/src/addon/calendar/pages/list/list.module.ts +++ b/src/addon/calendar/pages/list/list.module.ts @@ -15,9 +15,9 @@ 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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; +import { CorePipesModule } from '@pipes'; import { AddonCalendarListPage } from './list'; @NgModule({ diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index 85847215b..8f82a3258 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -17,15 +17,15 @@ import { IonicPage, Content, PopoverController, NavParams, NavController } from import { TranslateService } from '@ngx-translate/core'; import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarHelperProvider } from '../../providers/helper'; -import { CoreCoursesProvider } from '../../../../core/courses/providers/courses'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; -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'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +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'; /** * Page that displays the list of calendar events. diff --git a/src/addon/calendar/pages/settings/settings.module.ts b/src/addon/calendar/pages/settings/settings.module.ts index 59067baa6..93f443bd0 100644 --- a/src/addon/calendar/pages/settings/settings.module.ts +++ b/src/addon/calendar/pages/settings/settings.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { AddonCalendarSettingsPage } from './settings'; -import { CorePipesModule } from '../../../../pipes/pipes.module'; +import { CorePipesModule } from '@pipes'; @NgModule({ declarations: [ diff --git a/src/addon/calendar/pages/settings/settings.ts b/src/addon/calendar/pages/settings/settings.ts index 5cb59dda4..bebc4f1e9 100644 --- a/src/addon/calendar/pages/settings/settings.ts +++ b/src/addon/calendar/pages/settings/settings.ts @@ -15,8 +15,8 @@ import { Component } from '@angular/core'; import { IonicPage } from 'ionic-angular'; import { AddonCalendarProvider } from '../../providers/calendar'; -import { CoreEventsProvider } from '../../../../providers/events'; -import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; /** * Page that displays the calendar settings. diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 20532f254..741160a88 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -13,15 +13,15 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreSite } from '../../../classes/site'; -import { CoreCoursesProvider } from '../../../core/courses/providers/courses'; -import { CoreTimeUtilsProvider } from '../../../providers/utils/time'; -import { CoreGroupsProvider } from '../../../providers/groups'; -import { CoreConstants } from '../../../core/constants'; -import { CoreLocalNotificationsProvider } from '../../../providers/local-notifications'; -import { CoreConfigProvider } from '../../../providers/config'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSite } from '@classes/site'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreGroupsProvider } from '@providers/groups'; +import { CoreConstants } from '@core/constants'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { CoreConfigProvider } from '@providers/config'; /** * Service to handle calendar events. diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index 9c2bdcf9f..c1a0a8888 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -13,8 +13,8 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreCourseProvider } from '../../../core/course/providers/course'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreCourseProvider } from '@core/course/providers/course'; /** * Service that provides some features regarding lists of courses and categories. diff --git a/src/addon/calendar/providers/mainmenu-handler.ts b/src/addon/calendar/providers/mainmenu-handler.ts index 555e271fd..2769f0e0b 100644 --- a/src/addon/calendar/providers/mainmenu-handler.ts +++ b/src/addon/calendar/providers/mainmenu-handler.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { AddonCalendarProvider } from './calendar'; -import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../../core/mainmenu/providers/delegate'; +import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/providers/delegate'; /** * Handler to inject an option into main menu. @@ -22,7 +22,7 @@ import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../../core/main @Injectable() export class AddonCalendarMainMenuHandler implements CoreMainMenuHandler { name = 'AddonCalendar'; - priority = 400; + priority = 900; constructor(private calendarProvider: AddonCalendarProvider) { } diff --git a/src/addon/files/files.module.ts b/src/addon/files/files.module.ts index 56560abc2..25f500ee2 100644 --- a/src/addon/files/files.module.ts +++ b/src/addon/files/files.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { AddonFilesProvider } from './providers/files'; import { AddonFilesHelperProvider } from './providers/helper'; import { AddonFilesMainMenuHandler } from './providers/mainmenu-handler'; -import { CoreMainMenuDelegate } from '../../core/mainmenu/providers/delegate'; +import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; @NgModule({ declarations: [ diff --git a/src/addon/files/pages/list/list.module.ts b/src/addon/files/pages/list/list.module.ts index 50a40684d..f819f5512 100644 --- a/src/addon/files/pages/list/list.module.ts +++ b/src/addon/files/pages/list/list.module.ts @@ -15,8 +15,8 @@ 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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; import { AddonFilesListPage } from './list'; @NgModule({ diff --git a/src/addon/files/pages/list/list.ts b/src/addon/files/pages/list/list.ts index f79b3c19f..f2491f52f 100644 --- a/src/addon/files/pages/list/list.ts +++ b/src/addon/files/pages/list/list.ts @@ -15,11 +15,11 @@ import { Component, ViewChild, OnDestroy } from '@angular/core'; import { IonicPage, NavParams, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreAppProvider } from '../../../../providers/app'; -import { CoreEventsProvider } from '../../../../providers/events'; -import { CoreSitesProvider } from '../../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; -import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; +import { CoreAppProvider } from '@providers/app'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; import { AddonFilesProvider } from '../../providers/files'; import { AddonFilesHelperProvider } from '../../providers/helper'; diff --git a/src/addon/files/providers/files.ts b/src/addon/files/providers/files.ts index 268a835a3..a44a08d9c 100644 --- a/src/addon/files/providers/files.ts +++ b/src/addon/files/providers/files.ts @@ -13,9 +13,9 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreMimetypeUtilsProvider } from '../../../providers/utils/mimetype'; -import { CoreSite } from '../../../classes/site'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; +import { CoreSite } from '@classes/site'; import { Md5 } from 'ts-md5/dist/md5'; /** @@ -35,7 +35,7 @@ export class AddonFilesProvider { * @return {boolean} Whether the WS is available, false otherwise. */ canGetPrivateFilesInfo(): boolean { - return this.sitesProvider.getCurrentSite().wsAvailable('core_user_get_private_files_info'); + return this.sitesProvider.wsAvailableInCurrentSite('core_user_get_private_files_info'); } /** diff --git a/src/addon/files/providers/helper.ts b/src/addon/files/providers/helper.ts index 019cde0a0..4962b88e0 100644 --- a/src/addon/files/providers/helper.ts +++ b/src/addon/files/providers/helper.ts @@ -13,9 +13,9 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; -import { CoreFileUploaderHelperProvider } from '../../../core/fileuploader/providers/helper'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreFileUploaderHelperProvider } from '@core/fileuploader/providers/helper'; import { AddonFilesProvider } from './files'; /** diff --git a/src/addon/files/providers/mainmenu-handler.ts b/src/addon/files/providers/mainmenu-handler.ts index fb7e82d71..bb47f8cb5 100644 --- a/src/addon/files/providers/mainmenu-handler.ts +++ b/src/addon/files/providers/mainmenu-handler.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { AddonFilesProvider } from './files'; -import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../../core/mainmenu/providers/delegate'; +import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/providers/delegate'; /** * Handler to inject an option into main menu. @@ -22,7 +22,7 @@ import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../../core/main @Injectable() export class AddonFilesMainMenuHandler implements CoreMainMenuHandler { name = 'AddonFiles'; - priority = 200; + priority = 400; constructor(private filesProvider: AddonFilesProvider) { } diff --git a/src/addon/messages/components/components.module.ts b/src/addon/messages/components/components.module.ts new file mode 100644 index 000000000..270ed0e3a --- /dev/null +++ b/src/addon/messages/components/components.module.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 { 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'; +import { CorePipesModule } from '@pipes'; +import { AddonMessagesDiscussionsComponent } from '../components/discussions/discussions'; +import { AddonMessagesContactsComponent } from '../components/contacts/contacts'; + +@NgModule({ + declarations: [ + AddonMessagesDiscussionsComponent, + AddonMessagesContactsComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule + ], + providers: [ + ], + exports: [ + AddonMessagesDiscussionsComponent, + AddonMessagesContactsComponent + ] +}) +export class AddonMessagesComponentsModule {} diff --git a/src/addon/messages/components/contacts/contacts.html b/src/addon/messages/components/contacts/contacts.html new file mode 100644 index 000000000..a722ca171 --- /dev/null +++ b/src/addon/messages/components/contacts/contacts.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + +

{{ 'addon.messages.type_' + contactType | translate }}

+ {{ contacts[contactType].length }} +
+ + + + + + +

+
+
+
+
+
+
diff --git a/src/addon/messages/components/contacts/contacts.scss b/src/addon/messages/components/contacts/contacts.scss new file mode 100644 index 000000000..ff39202e3 --- /dev/null +++ b/src/addon/messages/components/contacts/contacts.scss @@ -0,0 +1,12 @@ +addon-messages-discussions { + h2 { + display: flex; + justify-content: space-between; + + .note { + margin: 0; + align-self: flex-end; + display: inline-flex; + } + } +} \ No newline at end of file diff --git a/src/addon/messages/components/contacts/contacts.ts b/src/addon/messages/components/contacts/contacts.ts new file mode 100644 index 000000000..a6cd49adf --- /dev/null +++ b/src/addon/messages/components/contacts/contacts.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 } from '@angular/core'; +import { NavParams } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreSitesProvider } from '@providers/sites'; +import { AddonMessagesProvider } from '../../providers/messages'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreAppProvider } from '@providers/app'; +import { CoreEventsProvider } from '@providers/events'; + +/** + * Component that displays the list of contacts. + */ +@Component({ + selector: 'addon-messages-contacts', + templateUrl: 'contacts.html', +}) +export class AddonMessagesContactsComponent { + + protected currentUserId: number; + protected searchingMessages: string; + protected loadingMessages: string; + protected siteId: string; + protected noSearchTypes = ['online', 'offline', 'blocked', 'strangers']; + + loaded = false; + discussionUserId: number; + contactTypes = this.noSearchTypes; + searchType = 'search'; + loadingMessage = ''; + hasContacts = false; + contacts = { + search: [] + }; + searchString = ''; + + constructor(sitesProvider: CoreSitesProvider, translate: TranslateService, private appProvider: CoreAppProvider, + private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams, + private eventsProvider: CoreEventsProvider) { + + this.currentUserId = sitesProvider.getCurrentSiteUserId(); + this.siteId = sitesProvider.getCurrentSiteId(); + this.searchingMessages = translate.instant('core.searching'); + this.loadingMessages = translate.instant('core.loading'); + this.loadingMessage = this.loadingMessages; + + this.discussionUserId = navParams.get('discussionUserId') || false; + } + + /** + * Component loaded. + */ + ngOnInit(): void { + if (this.discussionUserId) { + // There is a discussion to load, open the discussion in a new state. + this.gotoDiscussion(this.discussionUserId); + } + + this.fetchData().then(() => { + if (!this.discussionUserId && this.hasContacts) { + let contact; + for (const x in this.contacts) { + if (this.contacts[x].length > 0) { + contact = this.contacts[x][0]; + break; + } + } + + if (contact) { + // Take first and load it. + this.gotoDiscussion(contact.id, true); + } + } + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Refresh the data. + * + * @param {any} [refresher] Refresher. + * @return {Promise} Promise resolved when done. + */ + refreshData(refresher?: any): Promise { + let promise; + + if (this.searchString) { + // User has searched, update the search. + promise = this.performSearch(this.searchString); + } else { + // Update contacts. + promise = this.messagesProvider.invalidateAllContactsCache(this.currentUserId).then(() => { + return this.fetchData(); + }); + } + + return promise.finally(() => { + refresher.complete(); + }); + } + + /** + * Fetch contacts. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchData(): Promise { + this.loadingMessage = this.loadingMessages; + + return this.messagesProvider.getAllContacts().then((contacts) => { + for (const x in contacts) { + if (contacts[x].length > 0) { + this.contacts[x] = this.sortUsers(contacts[x]); + } else { + this.contacts[x] = []; + } + } + + this.clearSearch(); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingcontacts', true); + }); + } + + /** + * Sort user list by fullname + * @param {any[]} list List to sort. + * @return {any[]} Sorted list. + */ + protected sortUsers(list: any[]): any[] { + return list.sort((a, b) => { + const compareA = a.fullname.toLowerCase(), + compareB = b.fullname.toLowerCase(); + + return compareA.localeCompare(compareB); + }); + } + + /** + * Clear search and show all contacts again. + */ + clearSearch(): void { + this.searchString = ''; // Reset searched string. + this.contactTypes = this.noSearchTypes; + + this.hasContacts = false; + for (const x in this.contacts) { + if (this.contacts[x].length > 0) { + this.hasContacts = true; + + return; + } + } + } + + /** + * Search users from the UI. + * + * @param {string} query Text to search for. + * @return {Promise} Resolved when done. + */ + search(query: string): Promise { + this.appProvider.closeKeyboard(); + + this.loaded = false; + this.loadingMessage = this.searchingMessages; + + return this.performSearch(query).finally(() => { + this.loaded = true; + }); + } + + /** + * Perform the search of users. + * + * @param {string} query Text to search for. + * @return {Promise} Resolved when done. + */ + protected performSearch(query: string): Promise { + return this.messagesProvider.searchContacts(query).then((result) => { + this.hasContacts = result.length > 0; + this.searchString = query; + this.contactTypes = ['search']; + + this.contacts['search'] = this.sortUsers(result); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingcontacts', true); + }); + } + + /** + * Navigate to a particular discussion. + * + * @param {number} discussionUserId Discussion Id to load. + * @param {boolean} [onlyWithSplitView=false] Only go to Discussion if split view is on. + */ + gotoDiscussion(discussionUserId: number, onlyWithSplitView: boolean = false): void { + this.discussionUserId = discussionUserId; + + const params = { + discussion: discussionUserId, + onlyWithSplitView: onlyWithSplitView + }; + this.eventsProvider.trigger(AddonMessagesProvider.SPLIT_VIEW_LOAD_EVENT, params, this.siteId); + } +} diff --git a/src/addon/messages/components/discussions/discussions.html b/src/addon/messages/components/discussions/discussions.html new file mode 100644 index 000000000..2e7f4f993 --- /dev/null +++ b/src/addon/messages/components/discussions/discussions.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + +

{{ 'core.searchresults' | translate }}

+ {{ search.results.length }} +
+ + + + +

+

+
+
+ + + + + + +

+ + +
{{discussion.message.timecreated / 1000 | coreDateDayOrTime}}
+
+
+

+

+
+
+
+
diff --git a/src/addon/messages/components/discussions/discussions.scss b/src/addon/messages/components/discussions/discussions.scss new file mode 100644 index 000000000..ff39202e3 --- /dev/null +++ b/src/addon/messages/components/discussions/discussions.scss @@ -0,0 +1,12 @@ +addon-messages-discussions { + h2 { + display: flex; + justify-content: space-between; + + .note { + margin: 0; + align-self: flex-end; + display: inline-flex; + } + } +} \ No newline at end of file diff --git a/src/addon/messages/components/discussions/discussions.ts b/src/addon/messages/components/discussions/discussions.ts new file mode 100644 index 000000000..8738cf76b --- /dev/null +++ b/src/addon/messages/components/discussions/discussions.ts @@ -0,0 +1,250 @@ +// (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 } from '@angular/core'; +import { Platform, NavParams } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { AddonMessagesProvider } from '../../providers/messages'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreAppProvider } from '@providers/app'; +import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate'; + +/** + * Component that displays the list of discussions. + */ +@Component({ + selector: 'addon-messages-discussions', + templateUrl: 'discussions.html', +}) +export class AddonMessagesDiscussionsComponent implements OnDestroy { + protected newMessagesObserver: any; + protected readChangedObserver: any; + protected cronObserver: any; + protected appResumeSubscription: any; + protected loadingMessages: string; + protected siteId: string; + + loaded = false; + loadingMessage: string; + discussions: any; + discussionUserId: number; + pushObserver: any; + search = { + enabled: false, + showResults: false, + results: [], + loading: '', + text: '' + }; + + constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, translate: TranslateService, + private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams, + private appProvider: CoreAppProvider, platform: Platform, utils: CoreUtilsProvider, + pushNotificationsDelegate: AddonPushNotificationsDelegate) { + + this.search.loading = translate.instant('core.searching'); + this.loadingMessages = translate.instant('core.loading'); + this.siteId = sitesProvider.getCurrentSiteId(); + + // Update discussions when new message is received. + this.newMessagesObserver = eventsProvider.on(AddonMessagesProvider.NEW_MESSAGE_EVENT, (data) => { + if (data.userId) { + const discussion = this.discussions.find((disc) => { + return disc.message.user == data.userId; + }); + + if (typeof discussion == 'undefined') { + this.loaded = false; + this.refreshData().finally(() => { + this.loaded = true; + }); + } else { + // An existing discussion has a new message, update the last message. + discussion.message.message = data.message; + discussion.message.timecreated = data.timecreated; + } + } + }, this.siteId); + + // Update discussions when a message is read. + this.readChangedObserver = eventsProvider.on(AddonMessagesProvider.READ_CHANGED_EVENT, (data) => { + if (data.userId) { + const discussion = this.discussions.find((disc) => { + return disc.message.user == data.userId; + }); + + if (typeof discussion != 'undefined') { + // A discussion has been read reset counter. + discussion.unread = false; + + // Discussions changed, invalidate them. + this.messagesProvider.invalidateDiscussionsCache(); + } + } + }, this.siteId); + + // Update discussions when cron read is executed. + this.cronObserver = eventsProvider.on(AddonMessagesProvider.READ_CRON_EVENT, (data) => { + this.refreshData(); + }, this.siteId); + + // Refresh the view when the app is resumed. + this.appResumeSubscription = platform.resume.subscribe(() => { + if (!this.loaded) { + return; + } + this.loaded = false; + this.refreshData(); + }); + + this.discussionUserId = navParams.get('discussionUserId') || false; + + // If a message push notification is received, refresh the view. + this.pushObserver = pushNotificationsDelegate.on('receive').subscribe((notification) => { + // New message received. If it's from current site, refresh the data. + if (utils.isFalseOrZero(notification.notif) && notification.site == this.siteId) { + this.refreshData(); + } + }); + } + + /** + * Component loaded. + */ + ngOnInit(): void { + if (this.discussionUserId) { + // There is a discussion to load, open the discussion in a new state. + this.gotoDiscussion(this.discussionUserId); + } + + this.fetchData().then(() => { + if (!this.discussionUserId && this.discussions.length > 0) { + // Take first and load it. + this.gotoDiscussion(this.discussions[0].message.user, undefined, true); + } + }); + } + + /** + * Refresh the data. + * + * @param {any} [refresher] Refresher. + * @return {Promise} Promise resolved when done. + */ + refreshData(refresher?: any): Promise { + return this.messagesProvider.invalidateDiscussionsCache().then(() => { + return this.fetchData().finally(() => { + if (refresher) { + // Actions to take if refresh comes from the user. + this.eventsProvider.trigger(AddonMessagesProvider.READ_CHANGED_EVENT, undefined, this.siteId); + refresher.complete(); + } + }); + }); + } + + /** + * Fetch discussions. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchData(): Promise { + this.loadingMessage = this.loadingMessages; + this.search.enabled = this.messagesProvider.isSearchMessagesEnabled(); + + return this.messagesProvider.getDiscussions().then((discussions) => { + // Convert to an array for sorting. + const discussionsSorted = []; + for (const userId in discussions) { + discussions[userId].unread = !!discussions[userId].unread; + discussionsSorted.push(discussions[userId]); + } + + this.discussions = discussionsSorted.sort((a, b) => { + return b.message.timecreated - a.message.timecreated; + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Clear search and show discussions again. + */ + clearSearch(): void { + this.loaded = false; + this.search.showResults = false; + this.search.text = ''; // Reset searched string. + this.fetchData().finally(() => { + this.loaded = true; + }); + } + + /** + * Search messages cotaining text. + * + * @param {string} query Text to search for. + * @return {Promise} Resolved when done. + */ + searchMessage(query: string): Promise { + this.appProvider.closeKeyboard(); + this.loaded = false; + this.loadingMessage = this.search.loading; + + return this.messagesProvider.searchMessages(query).then((searchResults) => { + this.search.showResults = true; + this.search.results = searchResults; + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Navigate to a particular discussion. + * + * @param {number} discussionUserId Discussion Id to load. + * @param {number} [messageId] Message to scroll after loading the discussion. Used when searching. + * @param {boolean} [onlyWithSplitView=false] Only go to Discussion if split view is on. + */ + gotoDiscussion(discussionUserId: number, messageId?: number, onlyWithSplitView: boolean = false): void { + this.discussionUserId = discussionUserId; + + const params = { + discussion: discussionUserId, + onlyWithSplitView: onlyWithSplitView + }; + if (messageId) { + params['message'] = messageId; + } + this.eventsProvider.trigger(AddonMessagesProvider.SPLIT_VIEW_LOAD_EVENT, params, this.siteId); + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.newMessagesObserver && this.newMessagesObserver.off(); + this.readChangedObserver && this.readChangedObserver.off(); + this.cronObserver && this.cronObserver.off(); + this.appResumeSubscription && this.appResumeSubscription.unsubscribe(); + this.pushObserver && this.pushObserver.unsubscribe(); + } +} diff --git a/src/addon/messages/lang/en.json b/src/addon/messages/lang/en.json new file mode 100644 index 000000000..a75924db0 --- /dev/null +++ b/src/addon/messages/lang/en.json @@ -0,0 +1,32 @@ +{ + "addcontact": "Add contact", + "blockcontact": "Block contact", + "blockcontactconfirm": "You will no longer receive messages from this contact.", + "blocknoncontacts": "Prevent non-contacts from messaging me", + "contactlistempty": "The contact list is empty", + "contactname": "Contact name", + "contacts": "Contacts", + "deletemessage": "Delete message", + "deletemessageconfirmation": "Are you sure you want to delete this message? It will only be deleted from your messaging history and will still be viewable by the user who sent or received the message.", + "errordeletemessage": "Error while deleting the message.", + "errorwhileretrievingcontacts": "Error while retrieving contacts from the server.", + "errorwhileretrievingdiscussions": "Error while retrieving discussions from the server.", + "errorwhileretrievingmessages": "Error while retrieving messages from the server.", + "messagenotsent": "The message was not sent. Please try again later.", + "message": "Message", + "messagepreferences": "Message preferences", + "messages": "Messages", + "newmessage": "New message", + "newmessages": "New messages", + "nomessages": "No messages", + "nousersfound": "No users found", + "removecontact": "Remove contact", + "removecontactconfirm": "Contact will be removed from your contacts list.", + "type_blocked": "Blocked", + "type_offline": "Offline", + "type_online": "Online", + "type_search": "Search results", + "type_strangers": "Others", + "unblockcontact": "Unblock contact", + "warningmessagenotsent": "Couldn't send message(s) to user {{user}}. {{error}}" +} \ No newline at end of file diff --git a/src/addon/messages/messages.module.ts b/src/addon/messages/messages.module.ts new file mode 100644 index 000000000..2878f5fed --- /dev/null +++ b/src/addon/messages/messages.module.ts @@ -0,0 +1,116 @@ +// (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 { Network } from '@ionic-native/network'; +import { AddonMessagesProvider } from './providers/messages'; +import { AddonMessagesOfflineProvider } from './providers/messages-offline'; +import { AddonMessagesSyncProvider } from './providers/sync'; +import { AddonMessagesMainMenuHandler } from './providers/mainmenu-handler'; +import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; +import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; +import { CoreUserDelegate } from '@core/user/providers/user-delegate'; +import { CoreCronDelegate } from '@providers/cron'; +import { AddonMessagesSendMessageUserHandler } from './providers/user-send-message-handler'; +import { AddonMessagesAddContactUserHandler } from './providers/user-add-contact-handler'; +import { AddonMessagesBlockContactUserHandler } from './providers/user-block-contact-handler'; +import { AddonMessagesDiscussionLinkHandler } from './providers/discussion-link-handler'; +import { AddonMessagesIndexLinkHandler } from './providers/index-link-handler'; +import { AddonMessagesSyncCronHandler } from './providers/sync-cron-handler'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreAppProvider } from '@providers/app'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { CoreSettingsDelegate } from '@core/settings/providers/delegate'; +import { AddonMessagesSettingsHandler } from './providers/settings-handler'; +import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate'; +import { CoreUtilsProvider } from '@providers/utils/utils'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + providers: [ + AddonMessagesProvider, + AddonMessagesOfflineProvider, + AddonMessagesSyncProvider, + AddonMessagesMainMenuHandler, + AddonMessagesSendMessageUserHandler, + AddonMessagesAddContactUserHandler, + AddonMessagesBlockContactUserHandler, + AddonMessagesDiscussionLinkHandler, + AddonMessagesIndexLinkHandler, + AddonMessagesSyncCronHandler, + AddonMessagesSettingsHandler + ] +}) +export class AddonMessagesModule { + constructor(mainMenuDelegate: CoreMainMenuDelegate, mainmenuHandler: AddonMessagesMainMenuHandler, + contentLinksDelegate: CoreContentLinksDelegate, indexLinkHandler: AddonMessagesIndexLinkHandler, + discussionLinkHandler: AddonMessagesDiscussionLinkHandler, sendMessageHandler: AddonMessagesSendMessageUserHandler, + userDelegate: CoreUserDelegate, cronDelegate: CoreCronDelegate, syncHandler: AddonMessagesSyncCronHandler, + network: Network, messagesSync: AddonMessagesSyncProvider, appProvider: CoreAppProvider, + localNotifications: CoreLocalNotificationsProvider, messagesProvider: AddonMessagesProvider, + sitesProvider: CoreSitesProvider, linkHelper: CoreContentLinksHelperProvider, + settingsHandler: AddonMessagesSettingsHandler, settingsDelegate: CoreSettingsDelegate, + pushNotificationsDelegate: AddonPushNotificationsDelegate, utils: CoreUtilsProvider, + addContactHandler: AddonMessagesAddContactUserHandler, blockContactHandler: AddonMessagesBlockContactUserHandler) { + // Register handlers. + mainMenuDelegate.registerHandler(mainmenuHandler); + contentLinksDelegate.registerHandler(indexLinkHandler); + contentLinksDelegate.registerHandler(discussionLinkHandler); + userDelegate.registerHandler(sendMessageHandler); + userDelegate.registerHandler(addContactHandler); + userDelegate.registerHandler(blockContactHandler); + cronDelegate.register(syncHandler); + cronDelegate.register(mainmenuHandler); + settingsDelegate.registerHandler(settingsHandler); + + // Sync some discussions when device goes online. + network.onConnect().subscribe(() => { + messagesSync.syncAllDiscussions(undefined, true); + }); + + const notificationClicked = (notification: any): void => { + messagesProvider.isMessagingEnabledForSite(notification.site).then(() => { + sitesProvider.isFeatureDisabled('$mmSideMenuDelegate_mmaMessages', notification.site).then((disabled) => { + if (disabled) { + // Messages are disabled, stop. + return; + } + + messagesProvider.invalidateDiscussionsCache().finally(() => { + linkHelper.goInSite(undefined, 'AddonMessagesIndexPage', undefined, notification.site); + }); + }); + }); + }; + + if (appProvider.isDesktop()) { + // Listen for clicks in simulated push notifications. + localNotifications.registerClick(AddonMessagesProvider.PUSH_SIMULATION_COMPONENT, notificationClicked); + } + + // Register push notification clicks. + pushNotificationsDelegate.on('click').subscribe((notification) => { + if (utils.isFalseOrZero(notification.notif)) { + notificationClicked(notification); + + return true; + } + }); + } +} diff --git a/src/addon/messages/pages/discussion/discussion.html b/src/addon/messages/pages/discussion/discussion.html new file mode 100644 index 000000000..f41c4d1de --- /dev/null +++ b/src/addon/messages/pages/discussion/discussion.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + {{ message.timecreated | coreFormatDate: "LL" }} + + + + {{ 'addon.messages.newmessages' | translate:{$a: title} }} + + + + + +

+ +

+ + {{ message.timecreated | coreFormatDate: "dftimedate" }} + + + + +
+
+
+ +
+
+ + + + + diff --git a/src/addon/messages/pages/discussion/discussion.module.ts b/src/addon/messages/pages/discussion/discussion.module.ts new file mode 100644 index 000000000..e0f82d74a --- /dev/null +++ b/src/addon/messages/pages/discussion/discussion.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 { AddonMessagesDiscussionPage } from './discussion'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; +import { CorePipesModule } from '@pipes'; + +@NgModule({ + declarations: [ + AddonMessagesDiscussionPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(AddonMessagesDiscussionPage), + TranslateModule.forChild() + ], +}) +export class AddonMessagesDiscussionPageModule {} diff --git a/src/addon/messages/pages/discussion/discussion.scss b/src/addon/messages/pages/discussion/discussion.scss new file mode 100644 index 000000000..ab9a66c03 --- /dev/null +++ b/src/addon/messages/pages/discussion/discussion.scss @@ -0,0 +1,109 @@ +// Messages. +$item-message-bg: $gray-lighter !default; +$item-message-note-text: $gray-dark !default; +$item-message-note-font-size: 75% !default; +$item-message-mine-bg: $blue-light !default; + + +page-addon-messages-discussion { + .addon-messages-discussion-container { + display: flex; + flex-direction: column; + padding-bottom: 15px; + } + + .addon-messages-date, + .addon-messages-unreadfrom { + margin-top: 10px; + } + + .addon-messages-unreadfrom { + color: $blue; + } + + // Message item. + .addon-message { + max-width: 80%; + border: 0; + border-radius: 16px; + padding: 10px; + margin: 4px; + background-color: $item-message-bg; + align-self: flex-start; + width: auto; + min-height: 0; + position: relative; + @include core-transition(width); + + &.activated { + background-color: darken($item-message-bg, 10%); + } + + &.item-block .item-inner { + border-bottom: 0; + padding: 0; + margin: 0; + } + + .label { + margin: 0; + padding: 0; + } + + .addon-message-text { + display: inline-flex; + } + + .note { + align-self: flex-end; + color: $item-message-note-text; + font-size: $item-message-note-font-size; + margin-left: 10px; + } + .addon-messages-delete-button { + min-height: initial; + line-height: initial; + margin: 0 0 0 10px; + height: auto; + -webkit-align-self: flex-end; + -ms-flex-item-align: end; + align-self: flex-end; + vertical-align: middle; + + .icon { + font-size: 1.4em; + line-height: initial; + } + } + } + + // Defines when an item-message is the user's. + .addon-message-mine { + background-color: $item-message-mine-bg; + align-self: flex-end; + max-width: 80%; + + &.activated { + background-color: darken($item-message-mine-bg, 10%); + } + + .spinner { + float: right; + margin-left: 5px; + margin-right: -3px; + margin-top: 2px; + margin-bottom: -2px; + + svg { + width: 16px; + height: 16px; + } + } + } + + .addon-message .item-content, + .addon-message-mine .item-content{ + background-color: transparent; + padding: 0; + } +} \ No newline at end of file diff --git a/src/addon/messages/pages/discussion/discussion.ts b/src/addon/messages/pages/discussion/discussion.ts new file mode 100644 index 000000000..49c2125c6 --- /dev/null +++ b/src/addon/messages/pages/discussion/discussion.ts @@ -0,0 +1,689 @@ +// (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, ViewChild } from '@angular/core'; +import { IonicPage, NavParams, NavController, Content } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { AddonMessagesProvider } from '../../providers/messages'; +import { AddonMessagesSyncProvider } from '../../providers/sync'; +import { CoreUserProvider } from '@core/user/providers/user'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreAppProvider } from '@providers/app'; +import { coreSlideInOut } from '@classes/animations'; +import { Md5 } from 'ts-md5/dist/md5'; +import * as moment from 'moment'; + +/** + * Page that displays a message discussion page. + */ +@IonicPage({ segment: 'addon-messages-discussion' }) +@Component({ + selector: 'page-addon-messages-discussion', + templateUrl: 'discussion.html', + animations: [coreSlideInOut] +}) +export class AddonMessagesDiscussionPage implements OnDestroy { + @ViewChild(Content) content: Content; + + protected siteId: string; + protected fetching: boolean; + protected polling; + protected logger; + + protected unreadMessageFrom = 0; + protected messagesBeingSent = 0; + protected pagesLoaded = 1; + protected lastMessage = {text: '', timecreated: 0}; + protected keepMessageMap = {}; + protected syncObserver: any; + protected oldContentHeight = 0; + + userId: number; + currentUserId: number; + title: string; + profileLink: string; + showProfileLink: boolean; + loaded = false; + showKeyboard = false; + canLoadMore = false; + messages = []; + showDelete = false; + canDelete = false; + scrollBottom = true; + + constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, navParams: NavParams, + private userProvider: CoreUserProvider, private navCtrl: NavController, private messagesSync: AddonMessagesSyncProvider, + private domUtils: CoreDomUtilsProvider, private messagesProvider: AddonMessagesProvider, logger: CoreLoggerProvider, + private utils: CoreUtilsProvider, private appProvider: CoreAppProvider, private translate: TranslateService) { + this.siteId = sitesProvider.getCurrentSiteId(); + this.currentUserId = sitesProvider.getCurrentSiteUserId(); + + this.logger = logger.getInstance('AddonMessagesDiscussionPage'); + + this.userId = navParams.get('userId'); + this.showKeyboard = navParams.get('showKeyboard'); + + // Refresh data if this discussion is synchronized automatically. + this.syncObserver = eventsProvider.on(AddonMessagesSyncProvider.AUTO_SYNCED, (data) => { + if (data.userId == this.userId) { + // Fetch messages. + this.fetchData(); + + // Show first warning if any. + if (data.warnings && data.warnings[0]) { + this.domUtils.showErrorModal(data.warnings[0]); + } + } + }, this.siteId); + } + + /** + * Adds a new message to the message list. + * + * @param {any} message Message to be added. + * @param {boolean} [keep=true] If set the keep flag or not. + */ + protected addMessage(message: any, keep: boolean = true): void { + // Use smallmessage instead of message ID because ID changes when a message is read. + message.hash = Md5.hashStr(message.smallmessage) + '#' + message.timecreated + '#' + message.useridfrom; + if (typeof this.keepMessageMap[message.hash] === 'undefined') { + // Message not added to the list. Add it now. + this.messages.push(message); + } + // Message needs to be kept in the list. + this.keepMessageMap[message.hash] = keep; + } + + /** + * Remove a message if it shouldn't be in the list anymore. + * + * @param {string} hash Hash of the message to be removed. + */ + protected removeMessage(hash: any): void { + if (this.keepMessageMap[hash]) { + // Selected to keep it, clear the flag. + this.keepMessageMap[hash] = false; + + return; + } + + delete this.keepMessageMap[hash]; + + const position = this.messages.findIndex((message) => { + return message.hash == hash; + }); + if (position > 0) { + this.messages.splice(position, 1); + } + } + + /** + * Runs when the page has loaded. This event only happens once per page being created. + * If a page leaves but is cached, then this event will not fire again on a subsequent viewing. + * Setup code for the page. + */ + ionViewDidLoad(): void { + // Disable the profile button if we're already coming from a profile. + const backViewPage = this.navCtrl.getPrevious() && this.navCtrl.getPrevious().component.name; + this.showProfileLink = !backViewPage || backViewPage !== 'CoreUserProfilePage'; + + // Get the user profile to retrieve the user fullname and image. + this.userProvider.getProfile(this.userId, undefined, true).then((user) => { + if (!this.title) { + this.title = user.fullname; + } + this.profileLink = user.profileimageurl; + }); + + // Synchronize messages if needed. + this.messagesSync.syncDiscussion(this.userId).catch(() => { + // Ignore errors. + }).then((warnings) => { + if (warnings && warnings[0]) { + this.domUtils.showErrorModal(warnings[0]); + } + + // Fetch the messages for the first time. + return this.fetchData().then(() => { + if (!this.title && this.messages.length) { + // Didn't receive the fullname via argument. Try to get it from messages. + // It's possible that name cannot be resolved when no messages were yet exchanged. + if (this.messages[0].useridto != this.currentUserId) { + this.title = this.messages[0].usertofullname || ''; + } else { + this.title = this.messages[0].userfromfullname || ''; + } + } + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true); + }).finally(() => { + this.checkCanDelete(); + this.resizeContent(); + this.loaded = true; + }); + }); + } + + /** + * Runs when 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.setPolling(); + } + + /** + * Runs when the page is about to leave and no longer be the active page. + */ + ionViewWillLeave(): void { + this.unsetPolling(); + } + + /** + * Convenience function to fetch messages. + * @return {Promise} Resolved when done. + */ + protected fetchData(): Promise { + this.logger.debug(`Polling new messages for discussion with user '${this.userId}'`); + if (this.messagesBeingSent > 0) { + // We do not poll while a message is being sent or we could confuse the user. + // Otherwise, his message would disappear from the list, and he'd have to wait for the interval to check for messages. + return Promise.reject(null); + } else if (this.fetching) { + // Already fetching. + return Promise.reject(null); + } + + this.fetching = true; + + // Wait for synchronization process to finish. + return this.messagesSync.waitForSync(this.userId).then(() => { + // Fetch messages. Invalidate the cache before fetching. + return this.messagesProvider.invalidateDiscussionCache(this.userId).catch(() => { + // Ignore errors. + }); + }).then(() => { + return this.getDiscussion(this.pagesLoaded); + }).then((messages) => { + // Check if we are at the bottom to scroll it after render. + this.scrollBottom = this.content.scrollHeight - this.content.scrollTop === this.content.contentHeight; + + if (this.messagesBeingSent > 0) { + // Ignore polling due to a race condition. + return Promise.reject(null); + } + + // Add new messages to the list and mark the messages that should still be displayed. + messages.forEach((message) => { + this.addMessage(message); + }); + + // Remove messages that shouldn't be in the list anymore. + for (const hash in this.keepMessageMap) { + this.removeMessage(hash); + } + + // Sort the messages. + this.messagesProvider.sortMessages(this.messages); + + // Notify that there can be a new message. + this.notifyNewMessage(); + + // Mark retrieved messages as read if they are not. + this.markMessagesAsRead(); + }).finally(() => { + this.fetching = false; + }); + } + + /** + * Get a discussion. Can load several "pages". + * + * @param {number} pagesToLoad Number of pages to load. + * @param {number} [lfReceivedUnread=0] Number of unread received messages already fetched, so fetch will be done from this. + * @param {number} [lfReceivedRead=0] Number of read received messages already fetched, so fetch will be done from this. + * @param {number} [lfSentUnread=0] Number of unread sent messages already fetched, so fetch will be done from this. + * @param {number} [lfSentRead=0] Number of read sent messages already fetched, so fetch will be done from this. + * @return {Promise} Resolved when done. + */ + protected getDiscussion(pagesToLoad: number, lfReceivedUnread: number = 0, lfReceivedRead: number = 0, lfSentUnread: number = 0, + lfSentRead: number = 0): Promise { + + // Only get offline messages if we're loading the first "page". + const excludePending = lfReceivedUnread > 0 || lfReceivedRead > 0 || lfSentUnread > 0 || lfSentRead > 0; + + // Get next messages. + return this.messagesProvider.getDiscussion(this.userId, excludePending, lfReceivedUnread, lfReceivedRead, lfSentUnread, + lfSentRead).then((result) => { + + pagesToLoad--; + if (pagesToLoad > 0 && result.canLoadMore) { + // More pages to load. Calculate new limit froms. + result.messages.forEach((message) => { + if (!message.pending) { + if (message.useridfrom == this.userId) { + if (message.read) { + lfReceivedRead++; + } else { + lfReceivedUnread++; + } + } else { + if (message.read) { + lfSentRead++; + } else { + lfSentUnread++; + } + } + } + }); + + // Get next messages. + return this.getDiscussion(pagesToLoad, lfReceivedUnread, lfReceivedRead, lfSentUnread, lfSentRead) + .then((nextMessages) => { + return result.messages.concat(nextMessages); + }); + } else { + // No more messages to load, return them. + this.canLoadMore = result.canLoadMore; + + return result.messages; + } + }); + } + + /** + * Mark messages as read. + */ + protected markMessagesAsRead(): void { + let readChanged = false; + const promises = []; + + if (this.messagesProvider.isMarkAllMessagesReadEnabled()) { + let messageUnreadFound = false; + // Mark all messages at a time if one messages is unread. + for (const x in this.messages) { + const message = this.messages[x]; + // If an unread message is found, mark all messages as read. + if (message.useridfrom != this.currentUserId && message.read == 0) { + messageUnreadFound = true; + break; + } + } + if (messageUnreadFound) { + this.setUnreadLabelPosition(); + promises.push(this.messagesProvider.markAllMessagesRead(this.userId).then(() => { + readChanged = true; + // Mark all messages as read. + this.messages.forEach((message) => { + message.read = 1; + }); + })); + } + } else { + this.setUnreadLabelPosition(); + // Mark each message as read one by one. + this.messages.forEach((message) => { + // If the message is unread, call this.messagesProvider.markMessageRead. + if (message.useridfrom != this.currentUserId && message.read == 0) { + promises.push(this.messagesProvider.markMessageRead(message.id).then(() => { + readChanged = true; + message.read = 1; + })); + } + }); + } + + Promise.all(promises).finally(() => { + if (readChanged) { + this.eventsProvider.trigger(AddonMessagesProvider.READ_CHANGED_EVENT, { + userId: this.userId + }, this.siteId); + } + }); + } + + /** + * Notify the last message found so discussions list controller can tell if last message should be updated. + */ + protected notifyNewMessage(): void { + const last = this.messages[this.messages.length - 1]; + + let trigger = false; + if (!last) { + this.lastMessage = {text: '', timecreated: 0}; + trigger = true; + } else if (last.text !== this.lastMessage.text || last.timecreated !== this.lastMessage.timecreated) { + this.lastMessage = {text: last.text, timecreated: last.timecreated}; + trigger = true; + } + + if (trigger) { + // Update discussions last message. + this.eventsProvider.trigger(AddonMessagesProvider.NEW_MESSAGE_EVENT, { + userId: this.userId, + message: this.lastMessage.text, + timecreated: this.lastMessage.timecreated + }, this.siteId); + + // Update navBar links and buttons. + const newCanDelete = (last && last.id && this.messages.length == 1) || this.messages.length > 1; + if (this.canDelete != newCanDelete) { + this.checkCanDelete(); + } + } + } + + /** + * Set the place where the unread label position has to be. + */ + protected setUnreadLabelPosition(): void { + if (this.unreadMessageFrom != 0) { + return; + } + + let previousMessageRead = false; + + for (const x in this.messages) { + const message = this.messages[x]; + if (message.useridfrom != this.currentUserId) { + // Place unread from message label only once. + message.unreadFrom = message.read == 0 && previousMessageRead; + + if (message.unreadFrom) { + // Save where the label is placed. + this.unreadMessageFrom = parseInt(message.id, 10); + break; + } + + previousMessageRead = message.read != 0; + } + } + + // Do not update the message unread from label on next refresh. + if (this.unreadMessageFrom == 0) { + // Using negative to indicate the label is not placed but should not be placed. + this.unreadMessageFrom = -1; + } + } + + /** + * Check if there's any message in the list that can be deleted. + */ + protected checkCanDelete(): void { + // All messages being sent should be at the end of the list. + const first = this.messages[0]; + this.canDelete = first && !first.sending; + } + + /** + * Hide unread label when sending messages. + */ + protected hideUnreadLabel(): void { + if (this.unreadMessageFrom > 0) { + for (const x in this.messages) { + const message = this.messages[x]; + if (message.id == this.unreadMessageFrom) { + message.unreadFrom = false; + break; + } + } + + // Label hidden. + this.unreadMessageFrom = -1; + } + } + + /** + * Wait until fetching is false. + * @return {Promise} Resolved when done. + */ + protected waitForFetch(): Promise { + if (!this.fetching) { + return Promise.resolve(); + } + + const deferred = this.utils.promiseDefer(); + + setTimeout(() => { + return this.waitForFetch().finally(() => { + deferred.resolve(); + }); + }, 400); + + return deferred.promise; + } + + /** + * Set a polling to get new messages every certain time. + */ + protected setPolling(): void { + if (!this.polling) { + // Start polling. + this.polling = setInterval(() => { + this.fetchData().catch(() => { + // Ignore errors. + }); + }, AddonMessagesProvider.POLL_INTERVAL); + } + } + + /** + * Unset polling. + */ + protected unsetPolling(): void { + if (this.polling) { + this.logger.debug(`Cancelling polling for conversation with user '${this.userId}'`); + clearInterval(this.polling); + this.polling = undefined; + } + } + + /** + * Copy message to clipboard + * + * @param {string} text Message text to be copied. + */ + copyMessage(text: string): void { + this.utils.copyToClipboard(text); + } + + /** + * Function to delete a message. + * + * @param {any} message Message object to delete. + * @param {number} index Index where the mesasge is to delete it from the view. + */ + deleteMessage(message: any, index: number): void { + const langKey = message.pending ? 'core.areyousure' : 'addon.messages.deletemessageconfirmation'; + this.domUtils.showConfirm(this.translate.instant(langKey)).then(() => { + const modal = this.domUtils.showModalLoading('core.deleting', true); + + this.messagesProvider.deleteMessage(message).then(() => { + // Remove message from the list without having to wait for re-fetch. + this.messages.splice(index, 1); + this.removeMessage(message.hash); + this.notifyNewMessage(); + + this.fetchData(); // Re-fetch messages to update cached data. + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.messages.errordeletemessage', true); + }).finally(() => { + modal.dismiss(); + }); + }); + } + + /** + * Function to load previous messages. + * + * @param {any} [infiniteScroll] Infinite scroll object. + * @return {Promise} Resolved when done. + */ + loadPrevious(infiniteScroll: any): Promise { + // If there is an ongoing fetch, wait for it to finish. + return this.waitForFetch().finally(() => { + this.pagesLoaded++; + + this.fetchData().catch((error) => { + this.pagesLoaded--; + this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true); + }).finally(() => { + infiniteScroll.complete(); + }); + }); + } + + /** + * Content or scroll has been resized. For content, only call it if it's been added on top. + */ + resizeContent(): void { + let top = this.content.getContentDimensions().scrollTop; + this.content.resize(); + + // Wait for new content height to be calculated. + setTimeout(() => { + // Visible content size changed, maintain the bottom position. + if (this.content && this.content.contentHeight != this.oldContentHeight) { + if (!top) { + top = this.content.getContentDimensions().scrollTop; + } + + top += this.oldContentHeight - this.content.contentHeight; + this.oldContentHeight = this.content.contentHeight; + + this.content.scrollTo(0, top, 0); + } + }); + } + + /** + * Scroll bottom when render has finished. + */ + scrollToBottom(): void { + // Check if scroll is at bottom. If so, scroll bottom after rendering since there might be something new. + if (this.scrollBottom) { + // Need a timeout to leave time to the view to be rendered. + setTimeout(() => { + this.content.scrollToBottom(0); + }); + this.scrollBottom = false; + } + } + + /** + * Sends a message to the server. + * @param {string} text Message text. + */ + sendMessage(text: string): void { + let message; + + this.hideUnreadLabel(); + + this.showDelete = false; + this.scrollBottom = true; + + message = { + pending: true, + sending: true, + useridfrom: this.currentUserId, + smallmessage: text, + text: text, + timecreated: new Date().getTime() + }; + this.addMessage(message, false); + + this.messagesBeingSent++; + + // If there is an ongoing fetch, wait for it to finish. + // Otherwise, if a message is sent while fetching it could disappear until the next fetch. + this.waitForFetch().finally(() => { + this.messagesProvider.sendMessage(this.userId, text).then((data) => { + let promise; + + this.messagesBeingSent--; + + if (data.sent) { + // Message was sent, fetch messages right now. + promise = this.fetchData(); + } else { + promise = Promise.reject(null); + } + + promise.catch(() => { + // Fetch failed or is offline message, mark the message as sent. + // If fetch is successful there's no need to mark it because the fetch will already show the message received. + message.sending = false; + if (data.sent) { + // Message sent to server, not pending anymore. + message.pending = false; + } else if (data.message) { + message.timecreated = data.message.timecreated; + } + + this.notifyNewMessage(); + }); + }).catch((error) => { + this.messagesBeingSent--; + + // Only close the keyboard if an error happens. + // We want the user to be able to send multiple messages without the keyboard being closed. + this.appProvider.closeKeyboard(); + + this.domUtils.showErrorModalDefault(error, 'addon.messages.messagenotsent', true); + this.removeMessage(message.hash); + }); + }); + } + + /** + * Check date should be shown on message list for the current message. + * If date has changed from previous to current message it should be shown. + * + * @param {any} message Current message where to show the date. + * @param {any} [prevMessage] Previous message where to compare the date with. + * @return {boolean} If date has changed and should be shown. + */ + showDate(message: any, prevMessage?: any): boolean { + if (!prevMessage) { + // First message, show it. + return true; + } else if (message.pending) { + // If pending, it has no date, not show. + return false; + } + + // Check if day has changed. + return !moment(message.timecreated).isSame(prevMessage.timecreated, 'day'); + } + + /** + * Toggles delete state. + */ + toggleDelete(): void { + this.showDelete = !this.showDelete; + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + // Unset again, just in case. + this.unsetPolling(); + this.syncObserver && this.syncObserver.off(); + } +} diff --git a/src/addon/messages/pages/index/index.html b/src/addon/messages/pages/index/index.html new file mode 100644 index 000000000..9c9ef294a --- /dev/null +++ b/src/addon/messages/pages/index/index.html @@ -0,0 +1,22 @@ + + + {{ 'addon.messages.messages' | translate }} + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/addon/messages/pages/index/index.module.ts b/src/addon/messages/pages/index/index.module.ts new file mode 100644 index 000000000..169c8c7df --- /dev/null +++ b/src/addon/messages/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 { AddonMessagesIndexPage } from './index'; +import { CoreComponentsModule } from '@components/components.module'; +import { AddonMessagesComponentsModule } from '../../components/components.module'; + +@NgModule({ + declarations: [ + AddonMessagesIndexPage, + ], + imports: [ + CoreComponentsModule, + AddonMessagesComponentsModule, + IonicPageModule.forChild(AddonMessagesIndexPage), + TranslateModule.forChild() + ], +}) +export class AddonMessagesIndexPageModule {} diff --git a/src/addon/messages/pages/index/index.ts b/src/addon/messages/pages/index/index.ts new file mode 100644 index 000000000..e3b2975d2 --- /dev/null +++ b/src/addon/messages/pages/index/index.ts @@ -0,0 +1,71 @@ +// (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, ViewChild } from '@angular/core'; +import { IonicPage } from 'ionic-angular'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { AddonMessagesProvider } from '../../providers/messages'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; + +/** + * Page that displays the messages index page. + */ +@IonicPage({ segment: 'addon-messages-index' }) +@Component({ + selector: 'page-addon-messages-index', + templateUrl: 'index.html', +}) +export class AddonMessagesIndexPage implements OnDestroy { + @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; + + protected loadSplitViewObserver: any; + protected siteId: string; + + constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, + private messagesProvider: AddonMessagesProvider) { + + this.siteId = sitesProvider.getCurrentSiteId(); + + // Update split view or navigate. + this.loadSplitViewObserver = eventsProvider.on(AddonMessagesProvider.SPLIT_VIEW_LOAD_EVENT, (data) => { + if (data.discussion && (this.splitviewCtrl.isOn() || !data.onlyWithSplitView)) { + this.gotoDiscussion(data.discussion, data.message); + } + }, this.siteId); + } + + /** + * Navigate to a particular discussion. + * + * @param {number} discussionUserId Discussion Id to load. + * @param {number} [messageId] Message to scroll after loading the discussion. Used when searching. + */ + gotoDiscussion(discussionUserId: number, messageId?: number): void { + const params = { + userId: discussionUserId + }; + if (messageId) { + params['message'] = messageId; + } + this.splitviewCtrl.push('AddonMessagesDiscussionPage', params); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.loadSplitViewObserver && this.loadSplitViewObserver.off(); + } +} diff --git a/src/addon/messages/pages/settings/settings.html b/src/addon/messages/pages/settings/settings.html new file mode 100644 index 000000000..e9b27fbc1 --- /dev/null +++ b/src/addon/messages/pages/settings/settings.html @@ -0,0 +1,57 @@ + + + {{ 'addon.messages.messagepreferences' | translate }} + + + + + + + + + + + {{ 'addon.messages.blocknoncontacts' | translate }} + + + + + +
+ + + + {{ notification.displayname }} + {{ 'core.settings.loggedin' | translate }} + {{ 'core.settings.loggedoff' | translate }} + + + + + + {{ processor.displayname }} + + + + + + + {{ 'core.settings.disabled' | translate }} + + + + {{ processor.displayname }} + + + {{ 'core.settings.' + state | translate }} + + + + {{ 'core.settings.disabled' | translate }} + + + +
+
+
+
diff --git a/src/addon/messages/pages/settings/settings.module.ts b/src/addon/messages/pages/settings/settings.module.ts new file mode 100644 index 000000000..a20e5f382 --- /dev/null +++ b/src/addon/messages/pages/settings/settings.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 { AddonMessagesSettingsPage } from './settings'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; +import { AddonMessagesComponentsModule } from '../../components/components.module'; + +@NgModule({ + declarations: [ + AddonMessagesSettingsPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + AddonMessagesComponentsModule, + IonicPageModule.forChild(AddonMessagesSettingsPage), + TranslateModule.forChild() + ], +}) +export class AddonMessagesSettingsPageModule {} diff --git a/src/addon/messages/pages/settings/settings.scss b/src/addon/messages/pages/settings/settings.scss new file mode 100644 index 000000000..4b0e04a62 --- /dev/null +++ b/src/addon/messages/pages/settings/settings.scss @@ -0,0 +1,10 @@ +page-addon-messages-settings { + .list-header { + margin-bottom: 0; + border-top: 0; + } + + .toggle { + display: inline-block; + } +} \ No newline at end of file diff --git a/src/addon/messages/pages/settings/settings.ts b/src/addon/messages/pages/settings/settings.ts new file mode 100644 index 000000000..72a4e0b26 --- /dev/null +++ b/src/addon/messages/pages/settings/settings.ts @@ -0,0 +1,169 @@ +// (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 } from '@angular/core'; +import { IonicPage } from 'ionic-angular'; +import { AddonMessagesProvider } from '../../providers/messages'; +import { CoreUserProvider } from '@core/user/providers/user'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; + +/** + * Page that displays the messages settings page. + */ +@IonicPage({ segment: 'addon-messages-settings' }) +@Component({ + selector: 'page-addon-messages-settings', + templateUrl: 'settings.html', +}) +export class AddonMessagesSettingsPage implements OnDestroy { + protected updateTimeout: any; + + preferences: any; + preferencesLoaded: boolean; + blockNonContactsState = false; + + constructor(private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, + private userProvider: CoreUserProvider) { + } + + /** + * Runs when the page has loaded. This event only happens once per page being created. + * If a page leaves but is cached, then this event will not fire again on a subsequent viewing. + * Setup code for the page. + */ + ionViewDidLoad(): void { + this.fetchPreferences(); + } + + /** + * Fetches preference data. + * + * @return {Promise} Resolved when done. + */ + protected fetchPreferences(): Promise { + return this.messagesProvider.getMessagePreferences().then((preferences) => { + this.preferences = preferences; + this.blockNonContactsState = preferences.blocknoncontacts; + }).catch((message) => { + this.domUtils.showErrorModal(message); + }).finally(() => { + this.preferencesLoaded = true; + }); + } + + /** + * Update preferences. The purpose is to store the updated data, it won't be reflected in the view. + */ + protected updatePreferences(): void { + this.messagesProvider.invalidateMessagePreferences().finally(() => { + this.fetchPreferences(); + }); + } + + /** + * Update preferences after a certain time. The purpose is to store the updated data, it won't be reflected in the view. + */ + protected updatePreferencesAfterDelay(): void { + // Cancel pending updates. + clearTimeout(this.updateTimeout); + + this.updateTimeout = setTimeout(() => { + this.updateTimeout = null; + this.updatePreferences(); + }, 5000); + } + + /** + * Block non contacts. + * + * @param {boolean} block If it should be blocked or not. + */ + blockNonContacts(block: boolean): void { + const modal = this.domUtils.showModalLoading('core.sending', true); + this.userProvider.updateUserPreference('message_blocknoncontacts', block ? 1 : 0).then(() => { + // Update the preferences since they were modified. + this.updatePreferencesAfterDelay(); + }).catch((message) => { + // Show error and revert change. + this.domUtils.showErrorModal(message); + this.blockNonContactsState = !this.blockNonContactsState; + }).finally(() => { + modal.dismiss(); + }); + } + + /** + * Change the value of a certain preference. + * + * @param {any} notification Notification object. + * @param {string} state State name, ['loggedin', 'loggedoff']. + * @param {any} processor Notification processor. + */ + changePreference(notification: any, state: string, processor: any): void { + const processorState = processor[state], + preferenceName = notification.preferencekey + '_' + processorState.name, + valueArray = []; + let value = 'none'; + + notification.processors.forEach((processor) => { + if (processor[state].checked) { + valueArray.push(processor.name); + } + }); + + if (value.length > 0) { + value = valueArray.join(','); + } + + if (!notification.updating) { + notification.updating = {}; + } + + notification.updating[state] = true; + this.userProvider.updateUserPreference(preferenceName, value).then(() => { + // Update the preferences since they were modified. + this.updatePreferencesAfterDelay(); + }).catch((message) => { + // Show error and revert change. + this.domUtils.showErrorModal(message); + processorState.checked = !processorState.checked; + }).finally(() => { + notification.updating[state] = false; + }); + } + + /** + * Refresh the list of preferences. + * + * @param {any} refresher Refresher. + */ + refreshEvent(refresher: any): void { + this.messagesProvider.invalidateMessagePreferences().finally(() => { + this.fetchPreferences().finally(() => { + refresher.complete(); + }); + }); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + // If there is a pending action to update preferences, execute it right now. + if (this.updateTimeout) { + clearTimeout(this.updateTimeout); + this.updatePreferences(); + } + } +} diff --git a/src/addon/messages/providers/discussion-link-handler.ts b/src/addon/messages/providers/discussion-link-handler.ts new file mode 100644 index 000000000..869e9461b --- /dev/null +++ b/src/addon/messages/providers/discussion-link-handler.ts @@ -0,0 +1,89 @@ +// (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 { AddonMessagesProvider } from './messages'; +import { CoreSitesProvider } from '@providers/sites'; + +/** + * Content links handler for a discussion. + * Match message index URL with params id, user1 or user2. + */ +@Injectable() +export class AddonMessagesDiscussionLinkHandler extends CoreContentLinksHandlerBase { + name = 'AddonMessagesDiscussionLinkHandler'; + pattern = /\/message\/index\.php.*([\?\&](id|user1|user2)=\d+)/; + + constructor(private linkHelper: CoreContentLinksHelperProvider, private messagesProvider: AddonMessagesProvider, + private sitesProvider: CoreSitesProvider) { + 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 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); + } + }]; + } + + /** + * 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.messagesProvider.isPluginEnabled(siteId).then((enabled) => { + if (!enabled) { + return false; + } + + if (typeof params.id == 'undefined' && typeof params.user2 == 'undefined') { + // Other user not defined, cannot treat the URL. + return false; + } + + if (typeof params.user1 != 'undefined') { + // Check if user1 is the current user, since the app only supports current user. + return this.sitesProvider.getSite(siteId).then((site) => { + return parseInt(params.user1, 10) == site.getUserId(); + }); + } + + return true; + }); + } +} diff --git a/src/addon/messages/providers/index-link-handler.ts b/src/addon/messages/providers/index-link-handler.ts new file mode 100644 index 000000000..5b162b772 --- /dev/null +++ b/src/addon/messages/providers/index-link-handler.ts @@ -0,0 +1,66 @@ +// (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 { AddonMessagesProvider } from './messages'; + +/** + * Content links handler for messaging index. + * Match message index URL without params id, user1 or user2. + */ +@Injectable() +export class AddonMessagesIndexLinkHandler extends CoreContentLinksHandlerBase { + name = 'AddonMessagesIndexLinkHandler'; + pattern = /\/message\/index\.php((?![\?\&](id|user1|user2)=\d+).)*$/; + + constructor(private linkHelper: CoreContentLinksHelperProvider, private messagesProvider: AddonMessagesProvider) { + 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 => { + // Always use redirect to make it the new history root (to avoid "loops" in history). + this.linkHelper.goInSite(navCtrl, 'AddonMessagesIndexPage', undefined, 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.messagesProvider.isPluginEnabled(siteId); + } +} diff --git a/src/addon/messages/providers/mainmenu-handler.ts b/src/addon/messages/providers/mainmenu-handler.ts new file mode 100644 index 000000000..f49660b56 --- /dev/null +++ b/src/addon/messages/providers/mainmenu-handler.ts @@ -0,0 +1,208 @@ +// (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, Inject } from '@angular/core'; +import { AddonMessagesProvider } from './messages'; +import { CoreMainMenuDelegate, CoreMainMenuHandler, CoreMainMenuHandlerToDisplay } from '@core/mainmenu/providers/delegate'; +import { CoreCronHandler } from '@providers/cron'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreAppProvider } from '@providers/app'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { AddonPushNotificationsProvider } from '@addon/pushnotifications/providers/pushnotifications'; +import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate'; + +/** + * Handler to inject an option into main menu. + */ +@Injectable() +export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCronHandler { + name = 'AddonMessages'; + priority = 800; + protected badge = ''; + protected loading = true; + + constructor(private messagesProvider: AddonMessagesProvider, private sitesProvider: CoreSitesProvider, + private eventsProvider: CoreEventsProvider, private appProvider: CoreAppProvider, + private localNotificationsProvider: CoreLocalNotificationsProvider, private textUtils: CoreTextUtilsProvider, + private pushNotificationsProvider: AddonPushNotificationsProvider, utils: CoreUtilsProvider, + pushNotificationsDelegate: AddonPushNotificationsDelegate) { + + eventsProvider.on(AddonMessagesProvider.READ_CHANGED_EVENT, (data) => { + this.updateBadge(data.siteId); + }); + + eventsProvider.on(AddonMessagesProvider.READ_CRON_EVENT, (data) => { + this.updateBadge(data.siteId); + }); + + // Reset info on logout. + eventsProvider.on(CoreEventsProvider.LOGOUT, (data) => { + this.badge = ''; + this.loading = true; + }); + + // If a message push notification is received, refresh the count. + pushNotificationsDelegate.on('receive').subscribe((notification) => { + // New message received. If it's from current site, refresh the data. + if (utils.isFalseOrZero(notification.notif) && this.sitesProvider.isCurrentSite(notification.site)) { + this.updateBadge(notification.site); + } + }); + + // Register Badge counter. + pushNotificationsDelegate.registerCounterHandler('AddonMessages'); + } + + /** + * 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.messagesProvider.isPluginEnabled(); + } + + /** + * Returns the data needed to render the handler. + * + * @return {CoreMainMenuHandlerToDisplay} Data needed to render the handler. + */ + getDisplayData(): CoreMainMenuHandlerToDisplay { + if (this.loading) { + this.updateBadge(); + } + + return { + icon: 'chatbubbles', + title: 'addon.messages.messages', + page: 'AddonMessagesIndexPage', + class: 'addon-messages-handler', + showBadge: true, // Do not check isMessageCountEnabled because we'll use fallback it not enabled. + badge: this.badge, + loading: this.loading + }; + } + + /** + * Triggers an update for the badge number and loading status. Mandatory if showBadge is enabled. + * + * @param {string} siteId Site ID or current Site if undefined. + */ + updateBadge(siteId?: string): void { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + if (!siteId) { + return; + } + + this.messagesProvider.getUnreadConversationsCount(undefined, siteId).then((unread) => { + // Leave badge enter if there is a 0+ or a 0. + this.badge = parseInt(unread, 10) > 0 ? unread : ''; + // Update badge. + this.pushNotificationsProvider.updateAddonCounter('AddonMessages', unread, siteId); + }).catch(() => { + this.badge = ''; + }).finally(() => { + this.loading = false; + this.eventsProvider.trigger(CoreMainMenuDelegate.UPDATE_BADGE_EVENT, { + name: this.name, + badge: this.badge + }, siteId); + }); + } + + /** + * 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. + * @return {Promise} Promise resolved when done, rejected if failure. + */ + execute(siteId?: string): Promise { + if (this.sitesProvider.isCurrentSite(siteId)) { + this.eventsProvider.trigger(AddonMessagesProvider.READ_CRON_EVENT, undefined, siteId); + } + + if (this.appProvider.isDesktop() && this.localNotificationsProvider.isAvailable()) { + // @todo + /*$mmEmulatorHelper.checkNewNotifications( + AddonMessagesProvider.PUSH_SIMULATION_COMPONENT, this.fetchMessages, this.getTitleAndText, siteId);*/ + } + + return Promise.resolve(); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return this.appProvider.isDesktop() ? 60000 : 600000; // 1 or 10 minutes. + } + + /** + * Whether it's a synchronization process or not. + * + * @return {boolean} True if is a sync process, false otherwise. + */ + isSync(): boolean { + // This is done to use only wifi if using the fallback function. + // In desktop it is always sync, since it fetches messages to see if there's a new one. + return !this.messagesProvider.isMessageCountEnabled() || this.appProvider.isDesktop(); + } + + /** + * Whether the process should be executed during a manual sync. + * + * @return {boolean} True if is a manual sync process, false otherwise. + */ + canManualSync(): boolean { + return true; + } + + /** + * Get the latest unread received messages from a site. + * + * @param {string} [siteId] Site ID. Default current. + * @return {Promise} Promise resolved with the notifications. + */ + protected fetchMessages(siteId?: string): Promise { + return this.messagesProvider.getUnreadReceivedMessages(true, false, true, siteId).then((response) => { + return response.messages; + }); + } + + /** + * Given a message, return the title and the text for the message. + * + * @param {any} message Message. + * @return {Promise} Promise resolved with an object with title and text. + */ + protected getTitleAndText(message: any): Promise { + const data = { + title: message.userfromfullname, + }; + + return this.textUtils.formatText(message.text, true, true).catch(() => { + return message.text; + }).then((formattedText) => { + data['text'] = formattedText; + + return data; + }); + } +} diff --git a/src/addon/messages/providers/messages-offline.ts b/src/addon/messages/providers/messages-offline.ts new file mode 100644 index 000000000..ee8c5a7dd --- /dev/null +++ b/src/addon/messages/providers/messages-offline.ts @@ -0,0 +1,186 @@ +// (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 { CoreAppProvider } from '@providers/app'; + +/** + * Service to handle Offline messages. + */ +@Injectable() +export class AddonMessagesOfflineProvider { + + protected logger; + + // Variables for database. + protected MESSAGES_TABLE = 'addon_messages_offline_messages'; + protected tablesSchema = [ + { + name: this.MESSAGES_TABLE, + columns: [ + { + name: 'touserid', + type: 'INTEGER' + }, + { + name: 'useridfrom', + type: 'INTEGER' + }, + { + name: 'smallmessage', + type: 'TEXT' + }, + { + name: 'timecreated', + type: 'INTEGER' + }, + { + name: 'deviceoffline', // If message was stored because device was offline. + type: 'INTEGER' + } + ], + primaryKeys: ['touserid', 'smallmessage', 'timecreated'] + } + ]; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider) { + this.logger = logger.getInstance('AddonMessagesOfflineProvider'); + this.sitesProvider.createTablesFromSchema(this.tablesSchema); + } + + /** + * Delete a message. + * + * @param {number} toUserId User ID to send the message to. + * @param {string} message The message. + * @param {number} timeCreated The time the message was created. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + deleteMessage(toUserId: number, message: string, timeCreated: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().deleteRecords(this.MESSAGES_TABLE, { + touserid: toUserId, + smallmessage: message, + timecreated: timeCreated + }); + }); + } + + /** + * Get all messages where deviceoffline is set to 1. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with messages. + */ + getAllDeviceOfflineMessages(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(this.MESSAGES_TABLE, {deviceoffline: 1}); + }); + } + + /** + * Get offline messages to send to a certain user. + * + * @param {number} toUserId User ID to get messages to. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with messages. + */ + getMessages(toUserId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(this.MESSAGES_TABLE, {touserid: toUserId}); + }); + } + + /** + * Get all offline messages. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with messages. + */ + getAllMessages(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getAllRecords(this.MESSAGES_TABLE); + }); + } + + /** + * Check if there are offline messages to send to a certain user. + * + * @param {number} toUserId User ID to check. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if has offline messages, false otherwise. + */ + hasMessages(toUserId: number, siteId?: string): Promise { + return this.getMessages(toUserId, siteId).then((messages) => { + return !!messages.length; + }); + } + + /** + * Save a message to be sent later. + * + * @param {number} toUserId User ID recipient of the message. + * @param {string} message The message to send. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + saveMessage(toUserId: number, message: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const entry = { + touserid: toUserId, + useridfrom: site.getUserId(), + smallmessage: message, + timecreated: new Date().getTime(), + deviceoffline: this.appProvider.isOnline() ? 0 : 1 + }; + + return site.getDb().insertOrUpdateRecord(this.MESSAGES_TABLE, entry, { + touserid: toUserId, + smallmessage: message, + timecreated: entry.timecreated + }).then(() => { + return entry; + }); + }); + } + + /** + * Set deviceoffline for a group of messages. + * + * @param {any} messages Messages to update. Should be the same entry as retrieved from the DB. + * @param {boolean} value Value to set. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + setMessagesDeviceOffline(messages: any, value: boolean, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const db = site.getDb(), + promises = [], + data = { deviceoffline: value ? 1 : 0 }; + + messages.forEach((message) => { + promises.push(db.insertOrUpdateRecord(this.MESSAGES_TABLE, data, { + touserid: message.touserid, + smallmessage: message.smallmessage, + timecreated: message.timecreated + })); + }); + + return Promise.all(promises); + }); + } +} diff --git a/src/addon/messages/providers/messages.ts b/src/addon/messages/providers/messages.ts new file mode 100644 index 000000000..da898e64b --- /dev/null +++ b/src/addon/messages/providers/messages.ts @@ -0,0 +1,1168 @@ +// (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 { CoreAppProvider } from '@providers/app'; +import { CoreUserProvider } from '@core/user/providers/user'; +import { AddonMessagesOfflineProvider } from './messages-offline'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; + +/** + * Service to handle messages. + */ +@Injectable() +export class AddonMessagesProvider { + protected ROOT_CACHE_KEY = 'mmaMessages:'; + protected LIMIT_MESSAGES = 50; + protected LIMIT_SEARCH_MESSAGES = 50; + static NEW_MESSAGE_EVENT = 'addon_messages_new_message_event'; + static READ_CHANGED_EVENT = 'addon_messages_read_changed_event'; + static READ_CRON_EVENT = 'addon_messages_read_cron_event'; + static SPLIT_VIEW_LOAD_EVENT = 'addon_messages_split_view_load_event'; + static POLL_INTERVAL = 10000; + static PUSH_SIMULATION_COMPONENT = 'AddonMessagesPushSimulation'; + + protected logger; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, + private userProvider: CoreUserProvider, private messagesOffline: AddonMessagesOfflineProvider, + private utils: CoreUtilsProvider, private timeUtils: CoreTimeUtilsProvider) { + this.logger = logger.getInstance('AddonMessagesProvider'); + } + + /** + * Add a contact. + * + * @param {number} userId User ID of the person to add. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Resolved when done. + */ + addContact(userId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + userids: [ userId ] + }; + + return site.write('core_message_create_contacts', params).then(() => { + return this.invalidateAllContactsCache(site.getUserId(), site.getId()); + }); + }); + } + + /** + * Block a contact. + * + * @param {number} userId User ID of the person to block. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Resolved when done. + */ + blockContact(userId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + userids: [ userId ] + }; + + return site.write('core_message_block_contacts', params).then(() => { + return this.invalidateAllContactsCache(site.getUserId(), site.getId()); + }); + }); + } + + /** + * Delete a message (online or offline). + * + * @param {any} message Message to delete. + * @return {Promise} Promise resolved when the message has been deleted. + */ + deleteMessage(message: any): Promise { + if (message.id) { + // Message has ID, it means it has been sent to the server. + return this.deleteMessageOnline(message.id, message.read); + } + + // It's an offline message. + return this.messagesOffline.deleteMessage(message.touserid, message.smallmessage, message.timecreated); + } + + /** + * Delete a message from the server. + * + * @param {number} id Message ID. + * @param {number} read 1 if message is read, 0 otherwise. + * @param {number} [userId] User we want to delete the message for. If not defined, use current user. + * @return {Promise} Promise resolved when the message has been deleted. + */ + deleteMessageOnline(id: number, read: number, userId?: number): Promise { + userId = userId || this.sitesProvider.getCurrentSiteUserId(); + const params = { + messageid: id, + userid: userId, + read: read + }; + + return this.sitesProvider.getCurrentSite().write('core_message_delete_message', params).then(() => { + return this.invalidateDiscussionCache(userId); + }); + } + + /** + * Get the cache key for blocked contacts. + * + * @param {number} userId The user who's contacts we're looking for. + * @return {string} Cache key. + */ + protected getCacheKeyForBlockedContacts(userId: number): string { + return this.ROOT_CACHE_KEY + 'blockedContacts:' + userId; + } + + /** + * Get the cache key for contacts. + * + * @return {string} Cache key. + */ + protected getCacheKeyForContacts(): string { + return this.ROOT_CACHE_KEY + 'contacts'; + } + + /** + * Get the cache key for a discussion. + * + * @param {number} userId The other person with whom the current user is having the discussion. + * @return {string} Cache key. + */ + protected getCacheKeyForDiscussion(userId: number): string { + return this.ROOT_CACHE_KEY + 'discussion:' + userId; + } + + /** + * Get the cache key for the message count. + * + * @param {number} userId User ID. + * @return {string} Cache key. + */ + protected getCacheKeyForMessageCount(userId: number): string { + return this.ROOT_CACHE_KEY + 'count:' + userId; + } + + /** + * Get the cache key for the list of discussions. + * + * @return {string} Cache key. + */ + protected getCacheKeyForDiscussions(): string { + return this.ROOT_CACHE_KEY + 'discussions'; + } + + /** + * Get all the contacts of the current user. + * + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Resolved with the WS data. + */ + getAllContacts(siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.getContacts(siteId).then((contacts) => { + return this.getBlockedContacts(siteId).then((blocked) => { + contacts.blocked = blocked.users; + this.storeUsersFromAllContacts(contacts); + + return contacts; + }).catch(() => { + // The WS for blocked contacts might fail, but we still want the contacts. + contacts.blocked = []; + this.storeUsersFromAllContacts(contacts); + + return contacts; + }); + }); + } + + /** + * Get all the blocked contacts of the current user. + * + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Resolved with the WS data. + */ + getBlockedContacts(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const userId = site.getUserId(), + params = { + userid: userId + }, + preSets = { + cacheKey: this.getCacheKeyForBlockedContacts(userId) + }; + + return site.read('core_message_get_blocked_users', params, preSets); + }); + } + + /** + * Get the contacts of the current user. + * + * This excludes the blocked users. + * + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Resolved with the WS data. + */ + getContacts(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const preSets = { + cacheKey: this.getCacheKeyForContacts() + }; + + return site.read('core_message_get_contacts', undefined, preSets).then((contacts) => { + // Filter contacts with negative ID, they are notifications. + const validContacts = {}; + for (const typeName in contacts) { + if (!validContacts[typeName]) { + validContacts[typeName] = []; + } + + contacts[typeName].forEach((contact) => { + if (contact.id > 0) { + validContacts[typeName].push(contact); + } + }); + } + + return validContacts; + }); + }); + } + + /** + * Return the current user's discussion with another user. + * + * @param {number} userId The ID of the other user. + * @param {boolean} excludePending True to exclude messages pending to be sent. + * @param {number} [lfReceivedUnread=0] Number of unread received messages already fetched, so fetch will be done from this. + * @param {number} [lfReceivedRead=0] Number of read received messages already fetched, so fetch will be done from this. + * @param {number} [lfSentUnread=0] Number of unread sent messages already fetched, so fetch will be done from this. + * @param {number} [lfSentRead=0] Number of read sent messages already fetched, so fetch will be done from this. + * @param {boolean} [toDisplay=true] True if messages will be displayed to the user, either in view or in a notification. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Promise resolved with messages and a boolean telling if can load more messages. + */ + getDiscussion(userId: number, excludePending: boolean, lfReceivedUnread: number = 0, lfReceivedRead: number = 0, + lfSentUnread: number = 0, lfSentRead: number = 0, toDisplay: boolean = true, siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + const result = {}, + preSets = { + cacheKey: this.getCacheKeyForDiscussion(userId) + }, + params = { + useridto: site.getUserId(), + useridfrom: userId, + limitnum: this.LIMIT_MESSAGES + }; + + let hasReceived, + hasSent; + + if (lfReceivedUnread > 0 || lfReceivedRead > 0 || lfSentUnread > 0 || lfSentRead > 0) { + // Do not use cache when retrieving older messages. + // This is to prevent storing too much data and to prevent inconsistencies between "pages" loaded. + preSets['getFromCache'] = false; + preSets['saveToCache'] = false; + preSets['emergencyCache'] = false; + } + + // Get message received by current user. + return this.getRecentMessages(params, preSets, lfReceivedUnread, lfReceivedRead, toDisplay, site.getId()) + .then((response) => { + result['messages'] = response; + params.useridto = userId; + params.useridfrom = site.getUserId(); + hasReceived = response.length > 0; + + // Get message sent by current user. + return this.getRecentMessages(params, preSets, lfSentUnread, lfSentRead, toDisplay, siteId); + }).then((response) => { + result['messages'] = result['messages'].concat(response); + hasSent = response.length > 0; + + if (result['messages'].length > this.LIMIT_MESSAGES) { + // Sort messages and get the more recent ones. + result['canLoadMore'] = true; + result['messages'] = this.sortMessages(result['messages']); + result['messages'] = result['messages'].slice(-this.LIMIT_MESSAGES); + } else { + result['canLoadMore'] = result['messages'].length == this.LIMIT_MESSAGES && (!hasReceived || !hasSent); + } + + if (excludePending) { + // No need to get offline messages, return the ones we have. + return result; + } + + // Get offline messages. + return this.messagesOffline.getMessages(userId).then((offlineMessages) => { + // Mark offline messages as pending. + offlineMessages.forEach((message) => { + message.pending = true; + message.text = message.smallmessage; + }); + + result['messages'] = result['messages'].concat(offlineMessages); + + return result; + }); + }); + }); + } + + /** + * Get the discussions of the current user. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved with an object where the keys are the user ID of the other user. + */ + getDiscussions(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const discussions = {}, + currentUserId = site.getUserId(), + params = { + useridto: currentUserId, + useridfrom: 0, + limitnum: this.LIMIT_MESSAGES + }, + preSets = { + cacheKey: this.getCacheKeyForDiscussions() + }; + + /** + * Convenience function to treat a recent message, adding it to discussions list if needed. + */ + const treatRecentMessage = (message: any, userId: number, userFullname: string): void => { + if (typeof discussions[userId] === 'undefined') { + discussions[userId] = { + fullname: userFullname, + profileimageurl: '' + }; + + if (!message.timeread && !message.pending && message.useridfrom != currentUserId) { + discussions[userId].unread = true; + } + } + + // Extract the most recent message. Pending messages are considered more recent than messages already sent. + const discMessage = discussions[userId].message; + if (typeof discMessage === 'undefined' || (!discMessage.pending && message.pending) || + (discMessage.pending == message.pending && (discMessage.timecreated < message.timecreated || + (discMessage.timecreated == message.timecreated && discMessage.id < message.id)))) { + + discussions[userId].message = { + id: message.id, + user: userId, + message: message.text, + timecreated: message.timecreated, + pending: !!message.pending + }; + } + }; + + // Get recent messages sent to current user. + return this.getRecentMessages(params, preSets, undefined, undefined, undefined, site.getId()).then((messages) => { + + // Extract the discussions by filtering same senders. + messages.forEach((message) => { + treatRecentMessage(message, message.useridfrom, message.userfromfullname); + }); + + // Now get the last messages sent by the current user. + params.useridfrom = params.useridto; + params.useridto = 0; + + return this.getRecentMessages(params, preSets); + }).then((messages) => { + + // Extract the discussions by filtering same senders. + messages.forEach((message) => { + treatRecentMessage(message, message.useridto, message.usertofullname); + }); + + // Now get unsent messages. + return this.messagesOffline.getAllMessages(site.getId()); + }).then((offlineMessages) => { + offlineMessages.forEach((message) => { + message.pending = true; + message.text = message.smallmessage; + treatRecentMessage(message, message.touserid, ''); + }); + + return this.getDiscussionsUserImg(discussions, site.getId()).then((discussions) => { + this.storeUsersFromDiscussions(discussions); + + return discussions; + }); + }); + }); + } + + /** + * Get user images for all the discussions that don't have one already. + * + * @param {any} discussions List of discussions. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise always resolved. Resolve param is the formatted discussions. + */ + protected getDiscussionsUserImg(discussions: any, siteId?: string): Promise { + const promises = []; + + for (const userId in discussions) { + if (!discussions[userId].profileimageurl) { + // We don't have the user image. Try to retrieve it. + promises.push(this.userProvider.getProfile(discussions[userId].message.user, 0, true, siteId).then((user) => { + discussions[userId].profileimageurl = user.profileimageurl; + }).catch(() => { + // Error getting profile, resolve promise without adding any extra data. + })); + } + } + + return Promise.all(promises).then(() => { + return discussions; + }); + } + + /** + * Get the cache key for the get message preferences call. + * + * @return {string} Cache key. + */ + protected getMessagePreferencesCacheKey(): string { + return this.ROOT_CACHE_KEY + 'messagePreferences'; + } + + /** + * Get message preferences. + * + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Promise resolved with the message preferences. + */ + getMessagePreferences(siteId?: string): Promise { + this.logger.debug('Get message preferences'); + + return this.sitesProvider.getSite(siteId).then((site) => { + const preSets = { + cacheKey: this.getMessagePreferencesCacheKey() + }; + + return site.read('core_message_get_user_message_preferences', {}, preSets).then((data) => { + if (data.preferences) { + data.preferences.blocknoncontacts = !!data.blocknoncontacts; + + return data.preferences; + } + + return Promise.reject(null); + }); + }); + } + + /** + * Get messages according to the params. + * + * @param {any} params Parameters to pass to the WS. + * @param {any} preSets Set of presets for the WS. + * @param {boolean} [toDisplay=true] True if messages will be displayed to the user, either in view or in a notification. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} + */ + protected getMessages(params: any, preSets: any, toDisplay: boolean = true, siteId?: string): Promise { + params['type'] = 'conversations'; + params['newestfirst'] = 1; + + return this.sitesProvider.getSite(siteId).then((site) => { + const userId = site.getUserId(); + + return site.read('core_message_get_messages', params, preSets).then((response) => { + response.messages.forEach((message) => { + message.read = params.read == 0 ? 0 : 1; + // Convert times to milliseconds. + message.timecreated = message.timecreated ? message.timecreated * 1000 : 0; + message.timeread = message.timeread ? message.timeread * 1000 : 0; + }); + + if (toDisplay && this.appProvider.isDesktop() && !params.read && params.useridto == userId && + params.limitfrom === 0) { + // Store the last unread received messages. Don't block the user for this. + this.storeLastReceivedMessageIfNeeded(params.useridfrom, response.messages[0], site.getId()); + } + + return response; + }); + }); + } + + /** + * Get the most recent messages. + * + * @param {any} params Parameters to pass to the WS. + * @param {any} preSets Set of presets for the WS. + * @param {number} [limitFromUnread=0] Number of read messages already fetched, so fetch will be done from this number. + * @param {number} [limitFromRead=0] Number of unread messages already fetched, so fetch will be done from this number. + * @param {boolean} [toDisplay=true] True if messages will be displayed to the user, either in view or in a notification. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} + */ + protected getRecentMessages(params: any, preSets: any, limitFromUnread: number = 0, limitFromRead: number = 0, + toDisplay: boolean = true, siteId?: string): Promise { + limitFromUnread = limitFromUnread || 0; + limitFromRead = limitFromRead || 0; + + params['read'] = 0; + params['limitfrom'] = limitFromUnread; + + return this.getMessages(params, preSets, toDisplay, siteId).then((response) => { + let messages = response.messages; + if (messages) { + if (messages.length >= params.limitnum) { + return messages; + } + + // We need to fetch more messages. + params.limitnum = params.limitnum - messages.length; + params.read = 1; + params.limitfrom = limitFromRead; + + return this.getMessages(params, preSets, toDisplay, siteId).then((response) => { + if (response.messages) { + messages = messages.concat(response.messages); + } + + return messages; + }).catch(() => { + return messages; + }); + + } else { + return Promise.reject(null); + } + }); + } + + /** + * Get unread conversations count. Do not cache calls. + * + * @param {number} [userId] The user id who received the message. If not defined, use current user. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Promise resolved with the message unread count. + */ + getUnreadConversationsCount(userId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + // @since 3.2 + if (site.wsAvailable('core_message_get_unread_conversations_count')) { + const params = { + useridto: userId + }, + preSets = { + cacheKey: this.getCacheKeyForMessageCount(userId), + getFromCache: false, + emergencyCache: true, + saveToCache: true, + typeExpected: 'number' + }; + + return site.read('core_message_get_unread_conversations_count', params, preSets).catch(() => { + // Return no messages if the call fails. + return 0; + }); + } + + // Fallback call. + const params = { + read: 0, + limitfrom: 0, + limitnum: this.LIMIT_MESSAGES + 1, + useridto: userId, + useridfrom: 0, + }; + + return this.getMessages(params, undefined, false, siteId).then((response) => { + // Count the discussions by filtering same senders. + const discussions = {}; + + response.messages.forEach((message) => { + discussions[message.useridto] = 1; + }); + const count = Object.keys(discussions).length; + + // Add + sign if there are more than the limit reachable. + return (count > this.LIMIT_MESSAGES) ? count + '+' : count; + }).catch(() => { + // Return no messages if the call fails. + return 0; + }); + }); + } + + /** + * Get the latest unread received messages. + * + * @param {boolean} [toDisplay=true] True if messages will be displayed to the user, either in view or in a notification. + * @param {boolean} [forceCache] True if it should return cached data. Has priority over ignoreCache. + * @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, use current site. + * @return {Promise} Promise resolved with the message unread count. + */ + getUnreadReceivedMessages(toDisplay: boolean = true, forceCache: boolean = false, ignoreCache: boolean = false, + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + read: 0, + limitfrom: 0, + limitnum: this.LIMIT_MESSAGES, + useridto: site.getUserId(), + useridfrom: 0 + }, + preSets = {}; + + if (forceCache) { + preSets['omitExpires'] = true; + } else if (ignoreCache) { + preSets['getFromCache'] = false; + preSets['emergencyCache'] = false; + } + + return this.getMessages(params, preSets, toDisplay, siteId); + }); + } + + /** + * Invalidate all contacts cache. + * + * @param {number} userId The user ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when done. + */ + invalidateAllContactsCache(userId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.invalidateContactsCache(siteId).then(() => { + return this.invalidateBlockedContactsCache(userId, siteId); + }); + } + + /** + * Invalidate blocked contacts cache. + * + * @param {number} userId The user ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} + */ + invalidateBlockedContactsCache(userId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getCacheKeyForBlockedContacts(userId)); + }); + } + + /** + * Invalidate contacts cache. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when done. + */ + invalidateContactsCache(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getCacheKeyForContacts()); + }); + } + + /** + * Invalidate discussion cache. + * + * @param {number} userId The user ID with whom the current user is having the discussion. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when done. + */ + invalidateDiscussionCache(userId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getCacheKeyForDiscussion(userId)); + }); + } + + /** + * Invalidate discussions cache. + * + * Note that {@link this.getDiscussions} uses the contacts, so we need to invalidate contacts too. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when done. + */ + invalidateDiscussionsCache(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const promises = []; + promises.push(site.invalidateWsCacheForKey(this.getCacheKeyForDiscussions())); + promises.push(this.invalidateContactsCache(site.getId())); + + return Promise.all(promises); + }); + } + + /** + * Invalidate get message preferences. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when data is invalidated. + */ + invalidateMessagePreferences(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getMessagePreferencesCacheKey()); + }); + } + + /** + * Checks if the a user is blocked by the current user. + * + * @param {number} userId The user ID to check against. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Resolved with boolean, rejected when we do not know. + */ + isBlocked(userId: number, siteId?: string): Promise { + return this.getBlockedContacts(siteId).then((blockedContacts) => { + if (!blockedContacts.users || blockedContacts.users.length < 1) { + return false; + } + + return blockedContacts.users.some((user) => { + return userId == user.id; + }); + }); + } + + /** + * Checks if the a user is a contact of the current user. + * + * @param {number} userId The user ID to check against. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Resolved with boolean, rejected when we do not know. + */ + isContact(userId: number, siteId?: string): Promise { + return this.getContacts(siteId).then((contacts) => { + return ['online', 'offline'].some((type) => { + if (contacts[type] && contacts[type].length > 0) { + return contacts[type].some((user) => { + return userId == user.id; + }); + } + + return false; + }); + }); + } + + /** + * Returns whether or not we can mark all messages as read. + * + * @return {boolean} If related WS is avalaible on current site. + * @since 3.2 + */ + isMarkAllMessagesReadEnabled(): boolean { + return this.sitesProvider.wsAvailableInCurrentSite('core_message_mark_all_messages_as_read'); + } + + /** + * Returns whether or not we can count unread messages. + * + * @return {boolean} True if enabled, false otherwise. + * @since 3.2 + */ + isMessageCountEnabled(): boolean { + return this.sitesProvider.wsAvailableInCurrentSite('core_message_get_unread_conversations_count'); + } + + /** + * Returns whether or not the message preferences are enabled for the current site. + * + * @return {boolean} True if enabled, false otherwise. + * @since 3.2 + */ + isMessagePreferencesEnabled(): boolean { + return this.sitesProvider.wsAvailableInCurrentSite('core_message_get_user_message_preferences'); + } + + /** + * Returns whether or not messaging is enabled for a certain site. + * + * This could call a WS so do not abuse this method. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when enabled, otherwise rejected. + */ + isMessagingEnabledForSite(siteId?: string): Promise { + return this.isPluginEnabled(siteId).then((enabled) => { + if (!enabled) { + return Promise.reject(null); + } + }); + } + + /** + * Returns whether or not the plugin is enabled in a certain site. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with true if enabled, rejected or resolved with false otherwise. + */ + isPluginEnabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.canUseAdvancedFeature('messaging'); + }); + } + + /** + * Returns whether or not we can search messages. + * + * @return {boolean} + * @since 3.2 + */ + isSearchMessagesEnabled(): boolean { + return this.sitesProvider.wsAvailableInCurrentSite('core_message_data_for_messagearea_search_messages'); + } + + /** + * Mark message as read. + * + * @param {number} messageId ID of message to mark as read + * @returns {Promise} Promise resolved with boolean marking success or not. + */ + markMessageRead(messageId: number): Promise { + const params = { + messageid: messageId, + timeread: this.timeUtils.timestamp() + }; + + return this.sitesProvider.getCurrentSite().write('core_message_mark_message_read', params); + } + + /** + * Mark all messages of a discussion as read. + * + * @param {number} userIdFrom User Id for the sender. + * @returns {Promise} Promise resolved with boolean marking success or not. + */ + markAllMessagesRead(userIdFrom?: number): Promise { + const params = { + useridto: this.sitesProvider.getCurrentSiteUserId(), + useridfrom: userIdFrom + }, + preSets = { + typeExpected: 'boolean' + }; + + return this.sitesProvider.getCurrentSite().write('core_message_mark_all_messages_as_read', params, preSets); + } + + /** + * Remove a contact. + * + * @param {number} userId User ID of the person to remove. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Resolved when done. + */ + removeContact(userId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + userids: [ userId ] + }, + preSets = { + responseExpected: false + }; + + return site.write('core_message_delete_contacts', params, preSets).then(() => { + return this.invalidateContactsCache(site.getId()); + }); + }); + } + + /** + * Search for contacts. + * + * By default this only returns the first 100 contacts, but note that the WS can return thousands + * of results which would take a while to process. The limit here is just a convenience to + * prevent viewed to crash because too many DOM elements are created. + * + * @param {string} query The query string. + * @param {number} [limit=100] The number of results to return, 0 for none. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} + */ + searchContacts(query: string, limit: number = 100, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const data = { + searchtext: query, + onlymycourses: 0 + }, + preSets = { + getFromCache: false // Always try to get updated data. If it fails, it will get it from cache. + }; + + return site.read('core_message_search_contacts', data, preSets).then((contacts) => { + if (limit && contacts.length > limit) { + contacts = contacts.splice(0, limit); + } + this.userProvider.storeUsers(contacts); + + return contacts; + }); + }); + } + + /** + * Search for all the messges with a specific text. + * + * @param {string} query The query string + * @param {number} [userId] The user ID. If not defined, current user. + * @param {number} [from=0] Position of the first result to get. Defaults to 0. + * @param {number} [limit] Number of results to get. Defaults to LIMIT_SEARCH_MESSAGES. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the results. + */ + searchMessages(query: string, userId?: number, from: number = 0, limit: number = this.LIMIT_SEARCH_MESSAGES, siteId?: string): + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const param = { + userid: userId || site.getUserId(), + search: query, + limitfrom: from, + limitnum: limit + }, + preSets = { + getFromCache: false // Always try to get updated data. If it fails, it will get it from cache. + }; + + return site.read('core_message_data_for_messagearea_search_messages', param, preSets).then((searchResults) => { + return searchResults.contacts; + }); + }); + } + + /** + * Send a message to someone. + * + * @param {number} userIdTo User ID to send the message to. + * @param {string} message The message to send + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with: + * - sent (Boolean) True if message was sent to server, false if stored in device. + * - message (Object) If sent=false, contains the stored message. + */ + sendMessage(toUserId: number, message: string, siteId?: string): Promise { + // Convenience function to store a message to be synchronized later. + const storeOffline = (): Promise => { + return this.messagesOffline.saveMessage(toUserId, message, siteId).then((entry) => { + return { + sent: false, + message: entry + }; + }); + }; + + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (!this.appProvider.isOnline()) { + // App is offline, store the message. + return storeOffline(); + } + + // Check if this conversation already has offline messages. + // If so, store this message since they need to be sent in order. + return this.messagesOffline.hasMessages(toUserId, siteId).catch(() => { + // Error, it's safer to assume it has messages. + return true; + }).then((hasStoredMessages) => { + if (hasStoredMessages) { + return storeOffline(); + } + + // Online and no messages stored. Send it to server. + return this.sendMessageOnline(toUserId, message).then(() => { + return { sent: true }; + }).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 message, store it to retry later. + return storeOffline(); + }); + }); + } + + /** + * Send a message to someone. It will fail if offline or cannot connect. + * + * @param {number} toUserId User ID to send the message to. + * @param {string} message The message to send + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if success, rejected if failure. + */ + sendMessageOnline(toUserId: number, message: string, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const messages = [ + { + touserid: toUserId, + text: message, + textformat: 1 + } + ]; + + return this.sendMessagesOnline(messages, siteId).then((response) => { + if (response && response[0] && response[0].msgid === -1) { + // There was an error, and it should be translated already. + return this.utils.createFakeWSError(response[0].errormessage); + } + + return this.invalidateDiscussionCache(toUserId, siteId).catch(() => { + // Ignore errors. + }); + }); + } + + /** + * Send some messages. It will fail if offline or cannot connect. + * IMPORTANT: Sending several messages at once for the same discussions can cause problems with display order, + * since messages with same timecreated aren't ordered by ID. + * + * @param {any} messages Messages to send. Each message must contain touserid, text and textformat. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if success, rejected if failure. Promise resolved doesn't mean that messages + * have been sent, the resolve param can contain errors for messages not sent. + */ + sendMessagesOnline(messages: any, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const data = { + messages: messages + }; + + return site.write('core_message_send_instant_messages', data); + }); + } + + /** + * Helper method to sort messages by time. + * + * @param {any} messages Array of messages containing the key 'timecreated'. + * @return {any} Messages sorted with most recent last. + */ + sortMessages(messages: any): any { + return messages.sort((a, b) => { + // Pending messages last. + if (a.pending && !b.pending) { + return 1; + } else if (!a.pending && b.pending) { + return -1; + } + + const timecreatedA = parseInt(a.timecreated, 10), + timecreatedB = parseInt(b.timecreated, 10); + if (timecreatedA == timecreatedB && a.id) { + // Same time, sort by ID. + return a.id >= b.id ? 1 : -1; + } + + return timecreatedA >= timecreatedB ? 1 : -1; + }); + } + + /** + * Store the last received message if it's newer than the last stored. + * @todo + * + * @param {number} userIdFrom ID of the useridfrom retrieved, 0 for all users. + * @param {any} message Last message received. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + protected storeLastReceivedMessageIfNeeded(userIdFrom: number, message: any, siteId?: string): Promise { + /*let component = mmaMessagesPushSimulationComponent; + + // Get the last received message. + return $mmEmulatorHelper.getLastReceivedNotification(component, siteId).then((lastMessage) => { + if (userIdFrom > 0 && (!message || !lastMessage)) { + // Seeing a single discussion. No received message or cannot know if it really is the last received message. Stop. + return; + } + + if (message && lastMessage && message.timecreated <= lastMessage.timecreated) { + // The message isn't newer than the stored message, don't store it. + return; + } + + return $mmEmulatorHelper.storeLastReceivedNotification(component, message, siteId); + });*/ + return Promise.resolve(); + } + + /** + * Store user data from contacts in local DB. + * + * @param {any} contactTypes List of contacts grouped in types. + */ + protected storeUsersFromAllContacts(contactTypes: any): void { + for (const x in contactTypes) { + this.userProvider.storeUsers(contactTypes[x]); + } + } + + /** + * Store user data from discussions in local DB. + * + * @param {any} discussions List of discussions. + * @param {string} [siteId] Site ID. If not defined, current site. + */ + protected storeUsersFromDiscussions(discussions: any, siteId?: string): void { + const users = []; + for (const userId in discussions) { + users.push({ + id: userId, + fullname: discussions[userId].fullname, + profileimageurl: discussions[userId].profileimageurl + }); + } + this.userProvider.storeUsers(users, siteId); + } + + /** + * Unblock a user. + * + * @param {number} userId User ID of the person to unblock. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Resolved when done. + */ + unblockContact(userId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + userids: [ userId ] + }, + preSets = { + responseExpected: false + }; + + return site.write('core_message_unblock_contacts', params, preSets).then(() => { + return this.invalidateAllContactsCache(site.getUserId(), site.getId()); + }); + }); + } +} diff --git a/src/addon/messages/providers/settings-handler.ts b/src/addon/messages/providers/settings-handler.ts new file mode 100644 index 000000000..4f479f018 --- /dev/null +++ b/src/addon/messages/providers/settings-handler.ts @@ -0,0 +1,54 @@ +// (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, Inject } from '@angular/core'; +import { AddonMessagesProvider } from './messages'; +import { CoreSettingsHandler, CoreSettingsHandlerData } from '@core/settings/providers/delegate'; +import { CoreSitesProvider } from '@providers/sites'; + +/** + * Message settings handler. + */ +@Injectable() +export class AddonMessagesSettingsHandler implements CoreSettingsHandler { + name = 'AddonMessages'; + priority = 600; + + constructor(private messagesProvider: AddonMessagesProvider, private sitesProvider: CoreSitesProvider) { + } + + /** + * 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.messagesProvider.isMessagePreferencesEnabled(); + } + + /** + * Returns the data needed to render the handler. + * + * @return {CoreSettingsHandlerData} Data needed to render the handler. + */ + getDisplayData(): CoreSettingsHandlerData { + return { + icon: 'chatbubbles', + title: 'addon.messages.messagepreferences', + page: 'AddonMessagesSettingsPage', + class: 'addon-messages-settings-handler' + }; + } + +} diff --git a/src/addon/messages/providers/sync-cron-handler.ts b/src/addon/messages/providers/sync-cron-handler.ts new file mode 100644 index 000000000..4c643df83 --- /dev/null +++ b/src/addon/messages/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 { CoreSitesProvider } from '@providers/sites'; +import { AddonMessagesSyncProvider } from './sync'; + +/** + * Synchronization cron handler. + */ +@Injectable() +export class AddonMessagesSyncCronHandler implements CoreCronHandler { + name = 'AddonMessagesSyncCronHandler'; + + constructor(private sitesProvider: CoreSitesProvider, private messagesSync: AddonMessagesSyncProvider) {} + + /** + * 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. + * @return {Promise} Promise resolved when done, rejected if failure. + */ + execute(siteId?: string): Promise { + return this.messagesSync.syncAllDiscussions(siteId); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return 300000; // 5 minutes. + } +} diff --git a/src/addon/messages/providers/sync.ts b/src/addon/messages/providers/sync.ts new file mode 100644 index 000000000..45dcfbb24 --- /dev/null +++ b/src/addon/messages/providers/sync.ts @@ -0,0 +1,194 @@ +// (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 { AddonMessagesOfflineProvider } from './messages-offline'; +import { AddonMessagesProvider } from './messages'; +import { CoreUserProvider } from '@core/user/providers/user'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreSyncProvider } from '@providers/sync'; + +/** + * Service to sync messages. + */ +@Injectable() +export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { + + static AUTO_SYNCED = 'addon_messages_autom_synced'; + + constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider, + protected appProvider: CoreAppProvider, private messagesOffline: AddonMessagesOfflineProvider, + private eventsProvider: CoreEventsProvider, private messagesProvider: AddonMessagesProvider, + private userProvider: CoreUserProvider, private translate: TranslateService, private utils: CoreUtilsProvider, + syncProvider: CoreSyncProvider) { + super('AddonMessagesSync', sitesProvider, loggerProvider, appProvider, syncProvider); + } + + /** + * Try to synchronize all the discussions in a certain site or in all sites. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @param {boolean} [onlyDeviceOffline=false] True to only sync discussions that failed because device was offline, + * false to sync all. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllDiscussions(siteId?: string, onlyDeviceOffline: boolean = false): Promise { + const syncFunctionLog = 'all discussions' + (onlyDeviceOffline ? ' (Only offline)' : ''); + + return this.syncOnSites(syncFunctionLog, this.syncAllDiscussionsFunc.bind(this), [onlyDeviceOffline], siteId); + } + + /** + * Get all messages pending to be sent in the site. + * @param {boolean} [onlyDeviceOffline=false] True to only sync discussions that failed because device was offline, + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @param {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + protected syncAllDiscussionsFunc(onlyDeviceOffline: boolean = false, siteId?: string): Promise { + const promise = onlyDeviceOffline ? + this.messagesOffline.getAllDeviceOfflineMessages(siteId) : + this.messagesOffline.getAllMessages(siteId); + + return promise.then((messages) => { + const userIds = [], + promises = []; + + // Get all the discussions to be synced. + messages.forEach((message) => { + if (userIds.indexOf(message.touserid) == -1) { + userIds.push(message.touserid); + } + }); + + // Sync all discussions. + userIds.forEach((userId) => { + promises.push(this.syncDiscussion(userId, siteId).then((warnings) => { + if (typeof warnings != 'undefined') { + // Sync successful, send event. + this.eventsProvider.trigger(AddonMessagesSyncProvider.AUTO_SYNCED, { + userid: userId, + warnings: warnings + }, siteId); + } + })); + }); + + return Promise.all(promises); + }); + } + + /** + * Synchronize a discussion. + * + * @param {number} userId User ID of the discussion. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + syncDiscussion(userId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (this.isSyncing(userId, siteId)) { + // There's already a sync ongoing for this SCORM, return the promise. + return this.getOngoingSync(userId, siteId); + } + + const warnings = []; + + this.logger.debug(`Try to sync discussion with user '${userId}'`); + + // Get offline messages to be sent. + const syncPromise = this.messagesOffline.getMessages(userId, siteId).then((messages) => { + if (!messages.length) { + // Nothing to sync. + return []; + } else if (!this.appProvider.isOnline()) { + // Cannot sync in offline. Mark messages as device offline. + this.messagesOffline.setMessagesDeviceOffline(messages, true); + + return Promise.reject(null); + } + + let promise: Promise = Promise.resolve(); + const errors = []; + + // Order message by timecreated. + messages = this.messagesProvider.sortMessages(messages); + + // Send the messages. + // We don't use AddonMessagesProvider#sendMessagesOnline because there's a problem with display order. + // @todo Use AddonMessagesProvider#sendMessagesOnline once the display order is fixed. + messages.forEach((message, index) => { + // Chain message sending. If 1 message fails to be sent we'll stop sending. + promise = promise.then(() => { + return this.messagesProvider.sendMessageOnline(userId, message.smallmessage, siteId).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // Error returned by WS. Store the error to show a warning but keep sending messages. + if (errors.indexOf(error) == -1) { + errors.push(error); + } + + return; + } + + // Error sending, stop execution. + if (this.appProvider.isOnline()) { + // App is online, unmark deviceoffline if marked. + this.messagesOffline.setMessagesDeviceOffline(messages, false); + } + + return Promise.reject(error); + }).then(() => { + // Message was sent, delete it from local DB. + return this.messagesOffline.deleteMessage(userId, message.smallmessage, message.timecreated, siteId); + }).then(() => { + // All done. Wait 1 second to ensure timecreated of messages is different. + if (index < messages.length - 1) { + return setTimeout(() => {return; }, 1000); + } + }); + }); + }); + + return promise.then(() => { + return errors; + }); + }).then((errors) => { + if (errors && errors.length) { + // At least an error occurred, get user full name and add errors to warnings array. + return this.userProvider.getProfile(userId, undefined, true).catch(() => { + // Ignore errors. + return {}; + }).then((user) => { + errors.forEach((error) => { + warnings.push(this.translate.instant('addon.messages.warningmessagenotsent', { + user: user.fullname ? user.fullname : userId, + error: error + })); + }); + }); + } + }).then(() => { + // All done, return the warnings. + return warnings; + }); + + return this.addOngoingSync(userId, syncPromise, siteId); + } +} diff --git a/src/addon/messages/providers/user-add-contact-handler.ts b/src/addon/messages/providers/user-add-contact-handler.ts new file mode 100644 index 000000000..bae34daff --- /dev/null +++ b/src/addon/messages/providers/user-add-contact-handler.ts @@ -0,0 +1,170 @@ +// (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, OnDestroy } from '@angular/core'; +import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { AddonMessagesProvider } from './messages'; +import { AddonMessagesBlockContactUserHandler } from './user-block-contact-handler'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { TranslateService } from '@ngx-translate/core'; + +/** + * Profile add/remove contact handler. + */ +@Injectable() +export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandler, OnDestroy { + /** + * Update handler information event. + * @type {string} + */ + static UPDATED_EVENT = 'AddonMessagesAddContactUserHandler_updated_event'; + + name = 'AddonMessages:addContact'; + priority = 800; + type = CoreUserDelegate.TYPE_ACTION; + + protected disabled = false; + protected updateObs: any; + + constructor(private linkHelper: CoreContentLinksHelperProvider, protected sitesProvider: CoreSitesProvider, + private messagesProvider: AddonMessagesProvider, protected eventsProvider: CoreEventsProvider, + private domUtils: CoreDomUtilsProvider, private translate: TranslateService) { + + this.updateObs = eventsProvider.on(AddonMessagesBlockContactUserHandler.UPDATED_EVENT, (data) => { + this.checkButton(data.userId); + }); + } + + /** + * Check if handler is enabled. + * + * @return {Promise} Promise resolved with true if enabled, rejected or resolved with false otherwise. + */ + isEnabled(): Promise { + return this.messagesProvider.isPluginEnabled(); + } + + /** + * Check if handler is enabled for this user in this context. + * + * @param {any} user User to check. + * @param {number} courseId Course ID. + * @param {any} [navOptions] Course navigation options for current user. See $mmCourses#getUserNavigationOptions. + * @param {any} [admOptions] Course admin options for current user. See $mmCourses#getUserAdministrationOptions. + * @return {boolean|Promise} Promise resolved with true if enabled, resolved with false otherwise. + */ + isEnabledForUser(user: any, courseId: number, navOptions?: any, admOptions?: any): boolean | Promise { + return user.id != this.sitesProvider.getCurrentSiteUserId(); + } + + /** + * Returns the data needed to render the handler. + * + * @return {CoreUserProfileHandlerData} Data needed to render the handler. + */ + getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData { + this.checkButton(user.id); + + return { + icon: '', + title: '', + spinner: false, + class: '', + action: (event, navCtrl, user, courseId): void => { + event.preventDefault(); + event.stopPropagation(); + + if (this.disabled) { + return; + } + this.disabled = true; + this.updateButton({spinner: true}); + + this.messagesProvider.isContact(user.id).then((isContact) => { + if (isContact) { + const template = this.translate.instant('addon.messages.removecontactconfirm'), + title = this.translate.instant('addon.messages.removecontact'); + + return this.domUtils.showConfirm(template, title, title).then(() => { + return this.messagesProvider.removeContact(user.id); + }, () => { + // Ignore on cancel. + }); + } else { + return this.messagesProvider.addContact(user.id); + } + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.error', true); + }).finally(() => { + this.eventsProvider.trigger(AddonMessagesAddContactUserHandler.UPDATED_EVENT, {userId: user.id}); + this.checkButton(user.id).finally(() => { + this.disabled = false; + }); + }); + + } + }; + } + + /** + * Update Button with avalaible data. + * @param {number} userId User Id to update. + * @return {Promise} Promise resolved when done. + */ + protected checkButton(userId: number): Promise { + this.updateButton({spinner: true}); + + return this.messagesProvider.isContact(userId).then((isContact) => { + if (isContact) { + this.updateButton({ + title: 'addon.messages.removecontact', + class: 'addon-messages-removecontact-handler', + icon: 'remove', + hidden: false, + spinner: false + }); + } else { + this.updateButton({ + title: 'addon.messages.addcontact', + class: 'addon-messages-addcontact-handler', + icon: 'add', + hidden: false, + spinner: false + }); + } + }).catch(() => { + // This fails for some reason, let's just hide the button. + this.updateButton({hidden: true}); + }); + } + + /** + * Triggers the event to update the handler information. + * @param {any} data Data that should be updated. + */ + protected updateButton(data: any): void { + // This fails for some reason, let's just hide the button. + this.eventsProvider.trigger(CoreUserDelegate.UPDATE_HANDLER_EVENT, { handler: this.name, data: data }); + } + + /** + * Destroyed method. + */ + ngOnDestroy(): void { + this.updateObs && this.updateObs.off(); + } +} diff --git a/src/addon/messages/providers/user-block-contact-handler.ts b/src/addon/messages/providers/user-block-contact-handler.ts new file mode 100644 index 000000000..ead588acd --- /dev/null +++ b/src/addon/messages/providers/user-block-contact-handler.ts @@ -0,0 +1,171 @@ +// (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, OnDestroy } from '@angular/core'; +import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { AddonMessagesProvider } from './messages'; +import { AddonMessagesAddContactUserHandler } from './user-add-contact-handler'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { TranslateService } from '@ngx-translate/core'; + +/** + * Profile block/unblock contact handler. + */ +@Injectable() +export class AddonMessagesBlockContactUserHandler implements CoreUserProfileHandler, OnDestroy { + /** + * Update handler information event. + * @type {string} + */ + static UPDATED_EVENT = 'AddonMessagesBlockContactUserHandler_updated_event'; + + name = 'AddonMessages:blockContact'; + priority = 600; + type = CoreUserDelegate.TYPE_ACTION; + + protected disabled = false; + protected updateObs: any; + + constructor(private linkHelper: CoreContentLinksHelperProvider, protected sitesProvider: CoreSitesProvider, + private messagesProvider: AddonMessagesProvider, protected eventsProvider: CoreEventsProvider, + private domUtils: CoreDomUtilsProvider, private translate: TranslateService) { + + this.updateObs = eventsProvider.on(AddonMessagesAddContactUserHandler.UPDATED_EVENT, (data) => { + this.checkButton(data.userId); + }); + } + + /** + * Check if handler is enabled. + * + * @return {Promise} Promise resolved with true if enabled, rejected or resolved with false otherwise. + */ + isEnabled(): Promise { + return this.messagesProvider.isPluginEnabled(); + } + + /** + * Check if handler is enabled for this user in this context. + * + * @param {any} user User to check. + * @param {number} courseId Course ID. + * @param {any} [navOptions] Course navigation options for current user. See $mmCourses#getUserNavigationOptions. + * @param {any} [admOptions] Course admin options for current user. See $mmCourses#getUserAdministrationOptions. + * @return {boolean|Promise} Promise resolved with true if enabled, resolved with false otherwise. + */ + isEnabledForUser(user: any, courseId: number, navOptions?: any, admOptions?: any): boolean | Promise { + return user.id != this.sitesProvider.getCurrentSiteUserId(); + } + + /** + * Returns the data needed to render the handler. + * + * @return {CoreUserProfileHandlerData} Data needed to render the handler. + */ + getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData { + + this.checkButton(user.id); + + return { + icon: '', + title: '', + spinner: false, + class: '', + action: (event, navCtrl, user, courseId): void => { + event.preventDefault(); + event.stopPropagation(); + + if (this.disabled) { + return; + } + this.disabled = true; + this.updateButton({spinner: true}); + + this.messagesProvider.isBlocked(user.id).then((isBlocked) => { + if (isBlocked) { + return this.messagesProvider.unblockContact(user.id); + } else { + const template = this.translate.instant('addon.messages.blockcontactconfirm'), + title = this.translate.instant('addon.messages.blockcontact'); + + return this.domUtils.showConfirm(template, title, title).then(() => { + return this.messagesProvider.blockContact(user.id); + }, () => { + // Ignore on cancel. + }); + } + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.error', true); + }).finally(() => { + this.eventsProvider.trigger(AddonMessagesBlockContactUserHandler.UPDATED_EVENT, {userId: user.id}); + this.checkButton(user.id).finally(() => { + this.disabled = false; + }); + }); + + } + }; + } + + /** + * Update Button with avalaible data. + * @param {number} userId User Id to update. + * @return {Promise} Promise resolved when done. + */ + protected checkButton(userId: number): Promise { + this.updateButton({spinner: true}); + + return this.messagesProvider.isBlocked(userId).then((isBlocked) => { + if (isBlocked) { + this.updateButton({ + title: 'addon.messages.unblockcontact', + class: 'addon-messages-unblockcontact-handler', + icon: 'checkmark-circle', + hidden: false, + spinner: false + }); + } else { + this.updateButton({ + title: 'addon.messages.blockcontact', + class: 'addon-messages-blockcontact-handler', + icon: 'close-circle', + hidden: false, + spinner: false + }); + } + }).catch(() => { + // This fails for some reason, let's just hide the button. + this.updateButton({hidden: true}); + }); + } + + /** + * Triggers the event to update the handler information. + * @param {any} data Data that should be updated. + */ + protected updateButton(data: any): void { + // This fails for some reason, let's just hide the button. + this.eventsProvider.trigger(CoreUserDelegate.UPDATE_HANDLER_EVENT, { handler: this.name, data: data }); + } + + /** + * Destroyed method. + */ + ngOnDestroy(): void { + this.updateObs && this.updateObs.off(); + } +} diff --git a/src/addon/messages/providers/user-send-message-handler.ts b/src/addon/messages/providers/user-send-message-handler.ts new file mode 100644 index 000000000..1d1838c50 --- /dev/null +++ b/src/addon/messages/providers/user-send-message-handler.ts @@ -0,0 +1,77 @@ +// (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 { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { AddonMessagesProvider } from './messages'; + +/** + * Profile send message handler. + */ +@Injectable() +export class AddonMessagesSendMessageUserHandler implements CoreUserProfileHandler { + name = 'AddonMessages:sendMessage'; + priority = 1000; + type = CoreUserDelegate.TYPE_COMMUNICATION; + + constructor(private linkHelper: CoreContentLinksHelperProvider, protected sitesProvider: CoreSitesProvider, + private messagesProvider: AddonMessagesProvider) { } + + /** + * Check if handler is enabled. + * + * @return {Promise} Promise resolved with true if enabled, rejected or resolved with false otherwise. + */ + isEnabled(): Promise { + return this.messagesProvider.isPluginEnabled(); + } + + /** + * Check if handler is enabled for this user in this context. + * + * @param {any} user User to check. + * @param {number} courseId Course ID. + * @param {any} [navOptions] Course navigation options for current user. See $mmCourses#getUserNavigationOptions. + * @param {any} [admOptions] Course admin options for current user. See $mmCourses#getUserAdministrationOptions. + * @return {boolean|Promise} Promise resolved with true if enabled, resolved with false otherwise. + */ + isEnabledForUser(user: any, courseId: number, navOptions?: any, admOptions?: any): boolean | Promise { + return user.id != this.sitesProvider.getCurrentSiteUserId(); + } + + /** + * Returns the data needed to render the handler. + * + * @return {CoreUserProfileHandlerData} Data needed to render the handler. + */ + getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData { + return { + icon: 'send', + title: 'addon.messages.message', + class: 'addon-messages-send-message-handler', + action: (event, navCtrl, user, courseId): void => { + event.preventDefault(); + event.stopPropagation(); + const pageParams = { + 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/mod/book/book.module.ts b/src/addon/mod/book/book.module.ts index cebd7bb9e..e2d43fe6c 100644 --- a/src/addon/mod/book/book.module.ts +++ b/src/addon/mod/book/book.module.ts @@ -18,9 +18,9 @@ import { AddonModBookProvider } from './providers/book'; import { AddonModBookModuleHandler } from './providers/module-handler'; import { AddonModBookLinkHandler } from './providers/link-handler'; 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 { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; +import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; @NgModule({ declarations: [ diff --git a/src/addon/mod/book/components/components.module.ts b/src/addon/mod/book/components/components.module.ts index 1ea1ff532..cf175a12f 100644 --- a/src/addon/mod/book/components/components.module.ts +++ b/src/addon/mod/book/components/components.module.ts @@ -16,9 +16,9 @@ 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 { CoreCourseComponentsModule } from '../../../../core/course/components/components.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; +import { CoreCourseComponentsModule } from '@core/course/components/components.module'; import { AddonModBookIndexComponent } from './index/index'; import { AddonModBookTocPopoverComponent } from './toc-popover/toc-popover'; import { AddonModBookNavigationArrowsComponent } from './navigation-arrows/navigation-arrows'; diff --git a/src/addon/mod/book/components/index/index.ts b/src/addon/mod/book/components/index/index.ts index 48e2592ed..8e2b4bb92 100644 --- a/src/addon/mod/book/components/index/index.ts +++ b/src/addon/mod/book/components/index/index.ts @@ -15,12 +15,12 @@ import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, Optional } from '@angular/core'; import { NavParams, NavController, Content, PopoverController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreAppProvider } from '../../../../../providers/app'; -import { CoreDomUtilsProvider } from '../../../../../providers/utils/dom'; -import { CoreTextUtilsProvider } from '../../../../../providers/utils/text'; -import { CoreCourseProvider } from '../../../../../core/course/providers/course'; -import { CoreCourseHelperProvider } from '../../../../../core/course/providers/helper'; -import { CoreCourseModuleMainComponent } from '../../../../../core/course/providers/module-delegate'; +import { CoreAppProvider } from '@providers/app'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { CoreCourseModuleMainComponent } from '@core/course/providers/module-delegate'; import { AddonModBookProvider, AddonModBookContentsMap, AddonModBookTocChapter } from '../../providers/book'; import { AddonModBookPrefetchHandler } from '../../providers/prefetch-handler'; import { AddonModBookTocPopoverComponent } from '../../components/toc-popover/toc-popover'; diff --git a/src/addon/mod/book/pages/index/index.module.ts b/src/addon/mod/book/pages/index/index.module.ts index 02b409af8..bce50baa7 100644 --- a/src/addon/mod/book/pages/index/index.module.ts +++ b/src/addon/mod/book/pages/index/index.module.ts @@ -15,7 +15,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreDirectivesModule } from '../../../../../directives/directives.module'; +import { CoreDirectivesModule } from '@directives'; import { AddonModBookComponentsModule } from '../../components/components.module'; import { AddonModBookIndexPage } from './index'; diff --git a/src/addon/mod/book/providers/book.ts b/src/addon/mod/book/providers/book.ts index ba93df02a..b6d36e194 100644 --- a/src/addon/mod/book/providers/book.ts +++ b/src/addon/mod/book/providers/book.ts @@ -14,14 +14,14 @@ import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; -import { CoreFileProvider } from '../../../../providers/file'; -import { CoreFilepoolProvider } from '../../../../providers/filepool'; -import { CoreLoggerProvider } from '../../../../providers/logger'; -import { CoreSitesProvider } from '../../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; -import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; -import { CoreUtilsProvider } from '../../../../providers/utils/utils'; -import { CoreCourseProvider } from '../../../../core/course/providers/course'; +import { CoreFileProvider } from '@providers/file'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreCourseProvider } from '@core/course/providers/course'; /** * A book chapter inside the toc list. diff --git a/src/addon/mod/book/providers/link-handler.ts b/src/addon/mod/book/providers/link-handler.ts index 4013d4226..8f10bade7 100644 --- a/src/addon/mod/book/providers/link-handler.ts +++ b/src/addon/mod/book/providers/link-handler.ts @@ -13,8 +13,8 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreContentLinksModuleIndexHandler } from '../../../../core/contentlinks/classes/module-index-handler'; -import { CoreCourseHelperProvider } from '../../../../core/course/providers/helper'; +import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; /** * Handler to treat links to book. diff --git a/src/addon/mod/book/providers/module-handler.ts b/src/addon/mod/book/providers/module-handler.ts index 88fc25d7e..d88e95a24 100644 --- a/src/addon/mod/book/providers/module-handler.ts +++ b/src/addon/mod/book/providers/module-handler.ts @@ -16,8 +16,8 @@ import { Injectable } from '@angular/core'; import { NavController, NavOptions } from 'ionic-angular'; import { AddonModBookProvider } from './book'; import { AddonModBookIndexComponent } from '../components/index/index'; -import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '../../../../core/course/providers/module-delegate'; -import { CoreCourseProvider } from '../../../../core/course/providers/course'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; +import { CoreCourseProvider } from '@core/course/providers/course'; /** * Handler to support book modules. diff --git a/src/addon/mod/book/providers/prefetch-handler.ts b/src/addon/mod/book/providers/prefetch-handler.ts index 74bfd0aae..ca99a83fc 100644 --- a/src/addon/mod/book/providers/prefetch-handler.ts +++ b/src/addon/mod/book/providers/prefetch-handler.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; -import { CoreCourseModulePrefetchHandlerBase } from '../../../../core/course/classes/module-prefetch-handler'; +import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler'; import { AddonModBookProvider } from './book'; /** diff --git a/src/addon/mod/label/label.module.ts b/src/addon/mod/label/label.module.ts index f35ee2eaa..15be0e3cc 100644 --- a/src/addon/mod/label/label.module.ts +++ b/src/addon/mod/label/label.module.ts @@ -15,8 +15,8 @@ import { NgModule } from '@angular/core'; import { AddonModLabelModuleHandler } from './providers/module-handler'; import { AddonModLabelLinkHandler } from './providers/link-handler'; -import { CoreCourseModuleDelegate } from '../../../core/course/providers/module-delegate'; -import { CoreContentLinksDelegate } from '../../../core/contentlinks/providers/delegate'; +import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; +import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; @NgModule({ declarations: [ diff --git a/src/addon/mod/label/providers/link-handler.ts b/src/addon/mod/label/providers/link-handler.ts index 5dc547d8c..72ed0d615 100644 --- a/src/addon/mod/label/providers/link-handler.ts +++ b/src/addon/mod/label/providers/link-handler.ts @@ -13,8 +13,8 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreContentLinksModuleIndexHandler } from '../../../../core/contentlinks/classes/module-index-handler'; -import { CoreCourseHelperProvider } from '../../../../core/course/providers/helper'; +import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; /** * Handler to treat links to label. diff --git a/src/addon/mod/label/providers/module-handler.ts b/src/addon/mod/label/providers/module-handler.ts index 5b76b9818..90cfdd780 100644 --- a/src/addon/mod/label/providers/module-handler.ts +++ b/src/addon/mod/label/providers/module-handler.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '../../../../core/course/providers/module-delegate'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; /** * Handler to support label modules. diff --git a/src/addon/pushnotifications/providers/delegate.ts b/src/addon/pushnotifications/providers/delegate.ts new file mode 100644 index 000000000..61ca92ce5 --- /dev/null +++ b/src/addon/pushnotifications/providers/delegate.ts @@ -0,0 +1,107 @@ +// (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 { Subject } from 'rxjs'; + +/** + * Service to handle push notifications actions to perform when clicked and received. + */ +@Injectable() +export class AddonPushNotificationsDelegate { + + protected logger; + protected observables: { [s: string]: Subject } = {}; + protected counterHandlers: { [s: string]: string } = {}; + + constructor(loggerProvider: CoreLoggerProvider) { + this.logger = loggerProvider.getInstance('AddonPushNotificationsDelegate'); + this.observables['click'] = new Subject(); + this.observables['receive'] = new Subject(); + } + + /** + * Function called when a push notification is clicked. Sends notification to handlers. + * + * @param {any} notification Notification clicked. + */ + clicked(notification: any): void { + this.observables['click'].next(notification); + } + + /** + * Function called when a push notification is received in foreground (cannot tell when it's received in background). + * Sends notification to all handlers. + * + * @param {any} notification Notification received. + */ + received(notification: any): void { + this.observables['receive'].next(notification); + } + + /** + * Register a push notifications observable for click and receive notification event. + * When a notification is clicked or received, the observable will receive a notification to treat. + * let observer = pushNotificationsDelegate.on('click').subscribe((notification) => { + * ... + * observer.unsuscribe(); + * + * @param {string} eventName Only click and receive are permitted. + * @return {Subject} Observer to subscribe. + */ + on(eventName: string): Subject { + if (typeof this.observables[eventName] == 'undefined') { + const eventNames = Object.keys(this.observables).join(', '); + this.logger.warn(`'${eventName}' event name is not allowed. Use one of the following: '${eventNames}'.`); + + return new Subject(); + } + + return this.observables[eventName]; + } + + /** + * Register a push notifications handler for update badge counter. + * + * @param {string} name Handler's name. + */ + registerCounterHandler(name: string): void { + if (typeof this.counterHandlers[name] == 'undefined') { + this.logger.debug(`Registered handler '${name}' as badge counter handler.`); + this.counterHandlers[name] = name; + } else { + this.logger.log(`Handler '${name}' as badge counter handler already registered.`); + } + } + + /** + * Check if a counter handler is present. + * + * @param {string} name Handler's name. + * @return {boolean} If handler name is present. + */ + isCounterHandlerRegistered(name: string): boolean { + return typeof this.counterHandlers[name] != 'undefined'; + } + + /** + * Get all counter badge handlers. + * + * @return {any} with all the handler names. + */ + getCounterHandlers(): any { + return this.counterHandlers; + } +} diff --git a/src/addon/pushnotifications/providers/pushnotifications.ts b/src/addon/pushnotifications/providers/pushnotifications.ts new file mode 100644 index 000000000..75bbee3d4 --- /dev/null +++ b/src/addon/pushnotifications/providers/pushnotifications.ts @@ -0,0 +1,409 @@ +// (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 { Platform } from 'ionic-angular'; +import { Badge } from '@ionic-native/badge'; +import { Push, PushObject, PushOptions } from '@ionic-native/push'; +import { Device } from '@ionic-native/device'; +import { CoreAppProvider } from '@providers/app'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { AddonPushNotificationsDelegate } from './delegate'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreConfigProvider } from '@providers/config'; +import { CoreConfigConstants } from '.././../../configconstants'; + +/** + * Service to handle push notifications. + */ +@Injectable() +export class AddonPushNotificationsProvider { + + protected logger; + protected pushID: string; + protected appDB: any; + static COMPONENT = 'AddonPushNotificationsProvider'; + + // Variables for database. + protected BADGE_TABLE = 'addon_pushnotifications_badge'; + protected tablesSchema = [ + { + name: this.BADGE_TABLE, + columns: [ + { + name: 'siteid', + type: 'TEXT' + }, + { + name: 'addon', + type: 'TEXT' + }, + { + name: 'number', + type: 'INTEGER' + } + ], + primaryKeys: ['siteid', 'addon'] + } + ]; + + constructor(logger: CoreLoggerProvider, protected appProvider: CoreAppProvider, private platform: Platform, + protected pushNotificationsDelegate: AddonPushNotificationsDelegate, protected sitesProvider: CoreSitesProvider, + private badge: Badge, private localNotificationsProvider: CoreLocalNotificationsProvider, + private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider, private push: Push, + private configProvider: CoreConfigProvider, private device: Device) { + this.logger = logger.getInstance('AddonPushNotificationsProvider'); + this.appDB = appProvider.getDB(); + this.appDB.createTablesFromSchema(this.tablesSchema); + } + + /** + * Delete all badge records for a given site. + * + * @param {string} siteId Site ID. + * @return {Promise} Resolved when done. + */ + cleanSiteCounters(siteId: string): Promise { + return this.appDB.deleteRecords(this.BADGE_TABLE, {siteid: siteId} ).finally(() => { + this.updateAppCounter(); + }); + } + + /** + * Returns options for push notifications based on device. + * + * @return {Promise} Promise with the push options resolved when done. + */ + protected getOptions(): Promise { + // @todo: CoreSettingsProvider.NOTIFICATION_SOUND + return this.configProvider.get('CoreSettingsProvider.NOTIFICATION_SOUND', true).then((soundEnabled) => { + return { + android: { + senderID: CoreConfigConstants.gcmpn, + sound: !!soundEnabled + }, + ios: { + alert: 'true', + badge: true, + sound: !!soundEnabled + }, + windows: { + sound: !!soundEnabled + } + }; + }); + } + + /** + * Get the pushID for this device. + * + * @return {string} Push ID. + */ + getPushId(): string { + return this.pushID; + } + + /** + * Get Sitebadge counter from the database. + * + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved with the stored badge counter for the site. + */ + getSiteCounter(siteId: string): Promise { + return this.getAddonBadge(siteId); + } + + /** + * Function called when a push notification is clicked. Redirect the user to the right state. + * + * @param {any} notification Notification. + */ + notificationClicked(notification: any): void { + this.platform.ready().then(() => { + this.pushNotificationsDelegate.clicked(notification); + }); + } + + /** + * This function is called when we receive a Notification from APNS or a message notification from GCM. + * The app can be in foreground or background, + * if we are in background this code is executed when we open the app clicking in the notification bar. + * + * @param {any} notification Notification received. + */ + onMessageReceived(notification: any): void { + const data = notification ? notification.additionalData : {}; + + this.sitesProvider.getSite(data.site).then(() => { + if (this.utils.isTrueOrOne(data.foreground)) { + // If the app is in foreground when the notification is received, it's not shown. Let's show it ourselves. + if (this.localNotificationsProvider.isAvailable()) { + const localNotif = { + id: 1, + at: new Date(), + data: { + notif: data.notif, + site: data.site + }, + title: '', + text: '' + }, + promises = []; + + // Apply formatText to title and message. + promises.push(this.textUtils.formatText(notification.title, true, true).then((formattedTitle) => { + localNotif.title = formattedTitle; + }).catch(() => { + localNotif.title = notification.title; + })); + + promises.push(this.textUtils.formatText(notification.message, true, true).then((formattedMessage) => { + localNotif.text = formattedMessage; + }).catch(() => { + localNotif.text = notification.message; + })); + + Promise.all(promises).then(() => { + this.localNotificationsProvider.schedule(localNotif, AddonPushNotificationsProvider.COMPONENT, data.site); + }); + } + + // Trigger a notification received event. + this.platform.ready().then(() => { + data.title = notification.title; + data.message = notification.message; + this.pushNotificationsDelegate.received(data); + }); + } else { + // The notification was clicked. + // For compatibility with old push plugin implementation we'll merge all the notification data in a single object. + data.title = notification.title; + data.message = notification.message; + this.notificationClicked(data); + } + }); + } + + /** + * Unregisters a device from a certain Moodle site. + * + * @param {any} site Site to unregister from. + * @return {Promise} Promise resolved when device is unregistered. + */ + unregisterDeviceOnMoodle(site: any): Promise { + if (!site || !this.appProvider.isMobile()) { + return Promise.reject(null); + } + + this.logger.debug(`Unregister device on Moodle: '${site.id}'`); + + const data = { + appid: CoreConfigConstants.app_id, + uuid: this.device.uuid + }; + + return site.write('core_user_remove_user_device', data).then((response) => { + if (!response || !response.removed) { + return Promise.reject(null); + } + }); + } + + /** + * Update Counter for an addon. It will update the refered siteId counter and the total badge. + * It will return the updated addon counter. + * + * @param {string} addon Registered addon name to set the badge number. + * @param {number} value The number to be stored. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Promise resolved with the stored badge counter for the addon on the site. + */ + updateAddonCounter(addon: string, value: number, siteId?: string): Promise { + if (this.pushNotificationsDelegate.isCounterHandlerRegistered(addon)) { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.saveAddonBadge(value, siteId, addon).then(() => { + return this.updateSiteCounter(siteId).then(() => { + return value; + }); + }); + } + + return Promise.resolve(0); + } + + /** + * Update total badge counter of the app. + * + * @return {Promise} Promise resolved with the stored badge counter for the site. + */ + updateAppCounter(): Promise { + return this.sitesProvider.getSitesIds().then((sites) => { + const promises = []; + sites.forEach((siteId) => { + promises.push(this.getAddonBadge(siteId)); + }); + + return Promise.all(promises).then((counters) => { + const total = counters.reduce((previous, counter) => { + // The app badge counter does not support strings, so parse to int before. + return previous + parseInt(counter, 10); + }, 0); + + // Set the app badge. + return this.badge.set(total).then(() => { + return total; + }); + }); + }); + } + + /** + * Update counter for a site using the stored addon data. It will update the total badge application number. + * It will return the updated site counter. + * + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved with the stored badge counter for the site. + */ + updateSiteCounter(siteId: string): Promise { + const addons = this.pushNotificationsDelegate.getCounterHandlers(), + promises = []; + + for (const x in addons) { + promises.push(this.getAddonBadge(siteId, addons[x])); + } + + return Promise.all(promises).then((counters) => { + let plus = false, + total = counters.reduce((previous, counter) => { + // Check if there is a plus sign at the end of the counter. + if (counter != parseInt(counter, 10)) { + plus = true; + counter = parseInt(counter, 10); + } + + return previous + counter; + }, 0); + + total = plus && total > 0 ? total + '+' : total; + + // Save the counter on site. + return this.saveAddonBadge(total, siteId); + }).then((siteTotal) => { + return this.updateAppCounter().then(() => { + return siteTotal; + }); + }); + } + + /** + * Register a device in Apple APNS or Google GCM. + * + * @return {Promise} Promise resolved when the device is registered. + */ + registerDevice(): Promise { + try { + // Check if sound is enabled for notifications. + return this.getOptions().then((options) => { + const pushObject: PushObject = this.push.init(options); + + pushObject.on('notification').subscribe((notification: any) => { + this.logger.log('Received a notification', notification); + this.onMessageReceived(notification); + }); + + pushObject.on('registration').subscribe((registrationId: any) => { + this.pushID = registrationId; + this.registerDeviceOnMoodle().catch((error) => { + this.logger.warn('Can\'t register device', error); + }); + }); + + pushObject.on('error').subscribe((error: any) => { + this.logger.warn('Error with Push plugin', error); + }); + }); + } catch (ex) { + // Ignore errors. + this.logger.warn(ex); + } + + return Promise.reject(null); + } + + /** + * Registers a device on current Moodle site. + * + * @return {Promise} Promise resolved when device is registered. + */ + registerDeviceOnMoodle(): Promise { + this.logger.debug('Register device on Moodle.'); + + if (!this.sitesProvider.isLoggedIn() || !this.pushID || !this.appProvider.isMobile()) { + return Promise.reject(null); + } + + const data = { + appid: CoreConfigConstants.app_id, + name: this.device.manufacturer || '', + model: this.device.model, + platform: this.device.platform, + version: this.device.version, + pushid: this.pushID, + uuid: this.device.uuid + }; + + return this.sitesProvider.getCurrentSite().write('core_user_add_user_device', data); + } + + /** + * Get the addon/site badge counter from the database. + * + * @param {string} siteId Site ID. + * @param {string} [addon='site'] Registered addon name. If not defined it will store the site total. + * @return {Promise} Promise resolved with the stored badge counter for the addon or site or 0 if none. + */ + protected getAddonBadge(siteId?: string, addon: string = 'site'): Promise { + return this.appDB.getRecord(this.BADGE_TABLE, {siteid: siteId, addon: addon}).then((entry) => { + return (entry && entry.number) || 0; + }).catch(() => { + return 0; + }); + } + + /** + * Save the addon/site badgecounter on the database. + * + * @param {number} value The number to be stored. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @param {string} [addon='site'] Registered addon name. If not defined it will store the site total. + * @return {Promise} Promise resolved with the stored badge counter for the addon or site. + */ + protected saveAddonBadge(value: number, siteId?: string, addon: string = 'site'): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const entry = { + siteid: siteId, + addon: addon, + number: value + }; + + return this.appDB.insertOrUpdateRecord(this.BADGE_TABLE, entry, {siteid: siteId, addon: addon}).then(() => { + return value; + }); + } +} diff --git a/src/addon/pushnotifications/pushnotifications.module.ts b/src/addon/pushnotifications/pushnotifications.module.ts new file mode 100644 index 000000000..56dfd1abb --- /dev/null +++ b/src/addon/pushnotifications/pushnotifications.module.ts @@ -0,0 +1,67 @@ +// (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 { Platform } from 'ionic-angular'; +import { AddonPushNotificationsProvider } from './providers/pushnotifications'; +import { AddonPushNotificationsDelegate } from './providers/delegate'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + providers: [ + AddonPushNotificationsProvider, + AddonPushNotificationsDelegate + ] +}) +export class AddonPushNotificationsModule { + constructor(platform: Platform, pushNotificationsProvider: AddonPushNotificationsProvider, eventsProvider: CoreEventsProvider, + localNotificationsProvider: CoreLocalNotificationsProvider, loggerProvider: CoreLoggerProvider) { + + const logger = loggerProvider.getInstance('AddonPushNotificationsModule'); + + // Register device on GCM or APNS server. + platform.ready().then(() => { + pushNotificationsProvider.registerDevice(); + }); + + eventsProvider.on(CoreEventsProvider.NOTIFICATION_SOUND_CHANGED, () => { + // Notification sound has changed, register the device again to update the sound setting. + pushNotificationsProvider.registerDevice(); + }); + + // Register device on Moodle site when login. + eventsProvider.on(CoreEventsProvider.LOGIN, () => { + pushNotificationsProvider.registerDeviceOnMoodle().catch((error) => { + logger.warn('Can\'t register device', error); + }); + }); + + eventsProvider.on(CoreEventsProvider.SITE_DELETED, (site) => { + pushNotificationsProvider.unregisterDeviceOnMoodle(site).catch((error) => { + logger.warn('Can\'t unregister device', error); + }); + pushNotificationsProvider.cleanSiteCounters(site.id); + }); + + // Listen for local notification clicks (generated by the app). + localNotificationsProvider.registerClick(AddonPushNotificationsProvider.COMPONENT, + pushNotificationsProvider.notificationClicked); + } +} diff --git a/src/addon/userprofilefield/checkbox/checkbox.module.ts b/src/addon/userprofilefield/checkbox/checkbox.module.ts index c8358a518..ac828e8d4 100644 --- a/src/addon/userprofilefield/checkbox/checkbox.module.ts +++ b/src/addon/userprofilefield/checkbox/checkbox.module.ts @@ -16,9 +16,9 @@ import { NgModule } from '@angular/core'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { AddonUserProfileFieldCheckboxHandler } from './providers/handler'; -import { CoreUserProfileFieldDelegate } from '../../../core/user/providers/user-profile-field-delegate'; +import { CoreUserProfileFieldDelegate } from '@core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldCheckboxComponent } from './component/checkbox'; -import { CoreComponentsModule } from '../../../components/components.module'; +import { CoreComponentsModule } from '@components/components.module'; @NgModule({ declarations: [ diff --git a/src/addon/userprofilefield/checkbox/component/checkbox.ts b/src/addon/userprofilefield/checkbox/component/checkbox.ts index 839c38e88..998187701 100644 --- a/src/addon/userprofilefield/checkbox/component/checkbox.ts +++ b/src/addon/userprofilefield/checkbox/component/checkbox.ts @@ -14,7 +14,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { CoreUtilsProvider } from '../../../../providers/utils/utils'; +import { CoreUtilsProvider } from '@providers/utils/utils'; /** * Directive to render a checkbox user profile field. diff --git a/src/addon/userprofilefield/datetime/component/datetime.ts b/src/addon/userprofilefield/datetime/component/datetime.ts index 5c797ae92..1d43eaf80 100644 --- a/src/addon/userprofilefield/datetime/component/datetime.ts +++ b/src/addon/userprofilefield/datetime/component/datetime.ts @@ -14,8 +14,8 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { CoreTimeUtilsProvider } from '../../../../providers/utils/time'; -import { CoreUtilsProvider } from '../../../../providers/utils/utils'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; /** * Directive to render a datetime user profile field. diff --git a/src/addon/userprofilefield/datetime/datetime.module.ts b/src/addon/userprofilefield/datetime/datetime.module.ts index b1946bf1e..9814fc7e5 100644 --- a/src/addon/userprofilefield/datetime/datetime.module.ts +++ b/src/addon/userprofilefield/datetime/datetime.module.ts @@ -16,10 +16,10 @@ import { NgModule } from '@angular/core'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { AddonUserProfileFieldDatetimeHandler } from './providers/handler'; -import { CoreUserProfileFieldDelegate } from '../../../core/user/providers/user-profile-field-delegate'; +import { CoreUserProfileFieldDelegate } from '@core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldDatetimeComponent } from './component/datetime'; -import { CoreComponentsModule } from '../../../components/components.module'; -import { CorePipesModule } from '../../../pipes/pipes.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CorePipesModule } from '@pipes'; @NgModule({ declarations: [ diff --git a/src/addon/userprofilefield/menu/menu.module.ts b/src/addon/userprofilefield/menu/menu.module.ts index 89965cb30..0329b2c78 100644 --- a/src/addon/userprofilefield/menu/menu.module.ts +++ b/src/addon/userprofilefield/menu/menu.module.ts @@ -16,10 +16,10 @@ import { NgModule } from '@angular/core'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { AddonUserProfileFieldMenuHandler } from './providers/handler'; -import { CoreUserProfileFieldDelegate } from '../../../core/user/providers/user-profile-field-delegate'; +import { CoreUserProfileFieldDelegate } from '@core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldMenuComponent } from './component/menu'; -import { CoreComponentsModule } from '../../../components/components.module'; -import { CoreDirectivesModule } from '../../../directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/addon/userprofilefield/text/component/text.ts b/src/addon/userprofilefield/text/component/text.ts index e8a8ea696..84f1f44c1 100644 --- a/src/addon/userprofilefield/text/component/text.ts +++ b/src/addon/userprofilefield/text/component/text.ts @@ -14,7 +14,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { CoreUtilsProvider } from '../../../../providers/utils/utils'; +import { CoreUtilsProvider } from '@providers/utils/utils'; /** * Directive to render a text user profile field. diff --git a/src/addon/userprofilefield/text/providers/handler.ts b/src/addon/userprofilefield/text/providers/handler.ts index 1e75cc866..1953660ce 100644 --- a/src/addon/userprofilefield/text/providers/handler.ts +++ b/src/addon/userprofilefield/text/providers/handler.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '../../../../core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldTextComponent } from '../component/text'; -import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; /** * Text user profile field handlers. diff --git a/src/addon/userprofilefield/text/text.module.ts b/src/addon/userprofilefield/text/text.module.ts index c23f6bbea..a0cf647a6 100644 --- a/src/addon/userprofilefield/text/text.module.ts +++ b/src/addon/userprofilefield/text/text.module.ts @@ -16,10 +16,10 @@ import { NgModule } from '@angular/core'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { AddonUserProfileFieldTextHandler } from './providers/handler'; -import { CoreUserProfileFieldDelegate } from '../../../core/user/providers/user-profile-field-delegate'; +import { CoreUserProfileFieldDelegate } from '@core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldTextComponent } from './component/text'; -import { CoreComponentsModule } from '../../../components/components.module'; -import { CoreDirectivesModule } from '../../../directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/addon/userprofilefield/textarea/providers/handler.ts b/src/addon/userprofilefield/textarea/providers/handler.ts index 10044f151..1c07057db 100644 --- a/src/addon/userprofilefield/textarea/providers/handler.ts +++ b/src/addon/userprofilefield/textarea/providers/handler.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '../../../../core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldTextareaComponent } from '../component/textarea'; -import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; /** * Textarea user profile field handlers. diff --git a/src/addon/userprofilefield/textarea/textarea.module.ts b/src/addon/userprofilefield/textarea/textarea.module.ts index 6b5f696dc..1c2ce58ef 100644 --- a/src/addon/userprofilefield/textarea/textarea.module.ts +++ b/src/addon/userprofilefield/textarea/textarea.module.ts @@ -16,10 +16,10 @@ import { NgModule } from '@angular/core'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { AddonUserProfileFieldTextareaHandler } from './providers/handler'; -import { CoreUserProfileFieldDelegate } from '../../../core/user/providers/user-profile-field-delegate'; +import { CoreUserProfileFieldDelegate } from '@core/user/providers/user-profile-field-delegate'; import { AddonUserProfileFieldTextareaComponent } from './component/textarea'; -import { CoreComponentsModule } from '../../../components/components.module'; -import { CoreDirectivesModule } from '../../../directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 4bdeb273d..e3ba3417f 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -16,10 +16,10 @@ 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 { CoreLoggerProvider } from '../providers/logger'; -import { CoreLoginHelperProvider } from '../core/login/providers/helper'; +import { CoreAppProvider } from '@providers/app'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreLoginHelperProvider } from '@core/login/providers/helper'; @Component({ templateUrl: 'app.html' diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 188240118..7b0be5c01 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -13,6 +13,7 @@ // limitations under the License. import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NgModule } from '@angular/core'; import { IonicApp, IonicModule, Platform } from 'ionic-angular'; import { HttpModule } from '@angular/http'; @@ -22,53 +23,56 @@ import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { MoodleMobileApp } from './app.component'; -import { CoreInterceptor } from '../classes/interceptor'; -import { CoreLoggerProvider } from '../providers/logger'; -import { CoreDbProvider } from '../providers/db'; -import { CoreAppProvider } from '../providers/app'; -import { CoreConfigProvider } from '../providers/config'; -import { CoreLangProvider } from '../providers/lang'; -import { CoreTextUtilsProvider } from '../providers/utils/text'; -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 { CoreMimetypeUtilsProvider } from '../providers/utils/mimetype'; -import { CoreInitDelegate } from '../providers/init'; -import { CoreFileProvider } from '../providers/file'; -import { CoreWSProvider } from '../providers/ws'; -import { CoreEventsProvider } from '../providers/events'; -import { CoreSitesFactoryProvider } from '../providers/sites-factory'; -import { CoreSitesProvider } from '../providers/sites'; -import { CoreLocalNotificationsProvider } from '../providers/local-notifications'; -import { CoreGroupsProvider } from '../providers/groups'; -import { CoreCronDelegate } from '../providers/cron'; -import { CoreFileSessionProvider } from '../providers/file-session'; -import { CoreFilepoolProvider } from '../providers/filepool'; -import { CoreUpdateManagerProvider } from '../providers/update-manager'; -import { CorePluginFileDelegate } from '../providers/plugin-file-delegate'; -import { CoreSyncProvider } from '../providers/sync'; +import { CoreInterceptor } from '@classes/interceptor'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreDbProvider } from '@providers/db'; +import { CoreAppProvider } from '@providers/app'; +import { CoreConfigProvider } from '@providers/config'; +import { CoreLangProvider } from '@providers/lang'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +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 { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; +import { CoreInitDelegate } from '@providers/init'; +import { CoreFileProvider } from '@providers/file'; +import { CoreWSProvider } from '@providers/ws'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesFactoryProvider } from '@providers/sites-factory'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { CoreGroupsProvider } from '@providers/groups'; +import { CoreCronDelegate } from '@providers/cron'; +import { CoreFileSessionProvider } from '@providers/file-session'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreUpdateManagerProvider } from '@providers/update-manager'; +import { CorePluginFileDelegate } from '@providers/plugin-file-delegate'; +import { CoreSyncProvider } from '@providers/sync'; // Core modules. -import { CoreComponentsModule } from '../components/components.module'; -import { CoreEmulatorModule } from '../core/emulator/emulator.module'; -import { CoreLoginModule } from '../core/login/login.module'; -import { CoreMainMenuModule } from '../core/mainmenu/mainmenu.module'; -import { CoreCoursesModule } from '../core/courses/courses.module'; -import { CoreFileUploaderModule } from '../core/fileuploader/fileuploader.module'; -import { CoreSharedFilesModule } from '../core/sharedfiles/sharedfiles.module'; -import { CoreCourseModule } from '../core/course/course.module'; -import { CoreSiteHomeModule } from '../core/sitehome/sitehome.module'; -import { CoreContentLinksModule } from '../core/contentlinks/contentlinks.module'; -import { CoreUserModule } from '../core/user/user.module'; -import { CoreGradesModule } from '../core/grades/grades.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreEmulatorModule } from '@core/emulator/emulator.module'; +import { CoreLoginModule } from '@core/login/login.module'; +import { CoreMainMenuModule } from '@core/mainmenu/mainmenu.module'; +import { CoreCoursesModule } from '@core/courses/courses.module'; +import { CoreFileUploaderModule } from '@core/fileuploader/fileuploader.module'; +import { CoreSharedFilesModule } from '@core/sharedfiles/sharedfiles.module'; +import { CoreCourseModule } from '@core/course/course.module'; +import { CoreSiteHomeModule } from '@core/sitehome/sitehome.module'; +import { CoreContentLinksModule } from '@core/contentlinks/contentlinks.module'; +import { CoreUserModule } from '@core/user/user.module'; +import { CoreGradesModule } from '@core/grades/grades.module'; +import { CoreSettingsModule } from '@core/settings/settings.module'; // Addon modules. -import { AddonCalendarModule } from '../addon/calendar/calendar.module'; -import { AddonUserProfileFieldModule } from '../addon/userprofilefield/userprofilefield.module'; -import { AddonFilesModule } from '../addon/files/files.module'; -import { AddonModBookModule } from '../addon/mod/book/book.module'; -import { AddonModLabelModule } from '../addon/mod/label/label.module'; +import { AddonCalendarModule } from '@addon/calendar/calendar.module'; +import { AddonUserProfileFieldModule } from '@addon/userprofilefield/userprofilefield.module'; +import { AddonFilesModule } from '@addon/files/files.module'; +import { AddonModBookModule } from '@addon/mod/book/book.module'; +import { AddonModLabelModule } from '@addon/mod/label/label.module'; +import { AddonMessagesModule } from '@addon/messages/messages.module'; +import { AddonPushNotificationsModule } from '@addon/pushnotifications/pushnotifications.module'; // For translate loader. AoT requires an exported function for factories. export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { @@ -81,6 +85,7 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { ], imports: [ BrowserModule, + BrowserAnimationsModule, HttpClientModule, // HttpClient is used to make JSON requests. It fails for HEAD requests because there is no content. HttpModule, IonicModule.forRoot(MoodleMobileApp, { @@ -105,11 +110,14 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { CoreContentLinksModule, CoreUserModule, CoreGradesModule, + CoreSettingsModule, AddonCalendarModule, AddonUserProfileFieldModule, AddonFilesModule, AddonModBookModule, - AddonModLabelModule + AddonModLabelModule, + AddonMessagesModule, + AddonPushNotificationsModule ], bootstrap: [IonicApp], entryComponents: [ diff --git a/src/app/app.scss b/src/app/app.scss index e21adbc2e..d5fd9b7ce 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -15,7 +15,6 @@ // for the .md, .ios, or .wp mode classes. The mode class is // automatically applied to the element in the app. - // Alignment // ------------------------- @@ -51,7 +50,7 @@ } } -@include media-breakpoint-down(md) { +@include media-breakpoint-down(sm) { .hidden-phone { display: none !important; opacity: 0 !important; @@ -236,6 +235,18 @@ core-format-text[ng-reflect-max-height], *[core-format-text][ng-reflect-max-heig } } +core-format-text[singleLine="true"], *[core-format-text][singleLine="true"], +core-format-text[ng-reflect-single-line="true"], *[core-format-text][ng-reflect-single-line="true"] { + cursor: pointer; + pointer-events: auto; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; + position: relative; + width: 100%; +} + .core-media-adapt-width { max-width: 100%; } @@ -432,10 +443,52 @@ ion-toast.core-toast-alert .toast-wrapper{ background: $red-dark; } +textarea { + width: 100%; + resize: none; + + &[core-auto-rows] { + height: auto; + line-height: 18px; + padding: 5px; + } + + &:not([core-auto-rows]) { + height: 200px; + min-height: $core-rte-min-height; + } +} + +.toolbar .core-bar-button-image { + padding: 0; + width: 100%; + height: 100%; + max-width: $core-toolbar-button-image-width - 1; + max-height: $core-toolbar-button-image-width - 1; + border-radius: 50%; +} + +// Footer with auto height. +.footer.footer-adjustable { + height: auto; +} + // Message cards @each $color-name, $color-base, $color-contrast in get-colors($colors) { .core-#{$color-name}-card { @extend ion-card; border-bottom: 3px solid $color-base; } -} \ No newline at end of file +} + +.core-circle:before { + content: ' \25CF'; + font-size: 20px; +} + +@each $color-name, $color-base, $color-contrast in get-colors($colors) { + .core-#{$color-name}-circle:before { + @extend .core-circle:before; + color: $color-base; + } +} diff --git a/src/classes/animations.ts b/src/classes/animations.ts new file mode 100644 index 000000000..cb28a609b --- /dev/null +++ b/src/classes/animations.ts @@ -0,0 +1,65 @@ +// (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 { trigger, style, transition, animate, keyframes } from '@angular/animations'; + +export const coreShowHideAnimation = trigger('coreShowHideAnimation', [ + transition(':enter', [ + style({opacity: 0}), + animate('500ms ease-in-out', style({opacity: 1})) + ]), + transition(':leave', [ + style({opacity: 1}), + animate('500ms ease-in-out', style({opacity: 0})) + ]) +]); + +export const coreSlideInOut = trigger('coreSlideInOut', [ + // Enter animation. + transition('void => fromLeft', [ + style({transform: 'translateX(0)', opacity: 1}), + animate(300, keyframes([ + style({opacity: 0, transform: 'translateX(-100%)', offset: 0}), + style({opacity: 1, transform: 'translateX(5%)', offset: 0.7}), + style({opacity: 1, transform: 'translateX(0)', offset: 1.0}) + ])) + ]), + // Leave animation. + transition('fromLeft => void', [ + style({transform: 'translateX(-100%)', opacity: 0}), + animate(300, keyframes([ + style({opacity: 1, transform: 'translateX(0)', offset: 0}), + style({opacity: 1, transform: 'translateX(5%)', offset: 0.3}), + style({opacity: 0, transform: 'translateX(-100%)', offset: 1.0}) + ])) + ]), + // Enter animation. + transition('void => fromRight', [ + style({transform: 'translateX(0)', opacity: 1}), + animate(300, keyframes([ + style({opacity: 0, transform: 'translateX(100%)', offset: 0}), + style({opacity: 1, transform: 'translateX(-5%)', offset: 0.7}), + style({opacity: 1, transform: 'translateX(0)', offset: 1.0}) + ])) + ]), + // Leave animation. + transition('fromRight => void', [ + style({transform: 'translateX(-100%)', opacity: 0}), + animate(300, keyframes([ + style({opacity: 1, transform: 'translateX(0)', offset: 0}), + style({opacity: 1, transform: 'translateX(-5%)', offset: 0.3}), + style({opacity: 0, transform: 'translateX(100%)', offset: 1.0}) + ])) + ]) +]); diff --git a/src/classes/base-sync.ts b/src/classes/base-sync.ts index eaa905591..e1e70a2c5 100644 --- a/src/classes/base-sync.ts +++ b/src/classes/base-sync.ts @@ -12,13 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreSitesProvider } from '../providers/sites'; -import { CoreSyncProvider } from '../providers/sync'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreAppProvider } from '@providers/app'; /** * Base class to create sync providers. It provides some common functions. */ export class CoreSyncBaseProvider { + + /** + * Logger instance get from CoreLoggerProvider. + * @type {any} + */ + protected logger; + /** * Component of the sync provider. * @type {string} @@ -34,17 +43,21 @@ export class CoreSyncBaseProvider { // Store sync promises. protected syncPromises: { [siteId: string]: { [uniqueId: string]: Promise } } = {}; - constructor(private sitesProvider: CoreSitesProvider) { } + constructor(component: string, protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider, + protected appProvider: CoreAppProvider, protected syncProvider: CoreSyncProvider) { + this.logger = this.loggerProvider.getInstance(component); + this.component = component; + } /** * Add an ongoing sync to the syncPromises list. On finish the promise will be removed. * - * @param {number} id Unique sync identifier per component. + * @param {string | number} id Unique sync identifier per component. * @param {Promise} promise The promise of the sync to add. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} The sync promise. */ - addOngoingSync(id: number, promise: Promise, siteId?: string): Promise { + addOngoingSync(id: string | number, promise: Promise, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); const uniqueId = this.getUniqueSyncId(id); @@ -63,11 +76,11 @@ export class CoreSyncBaseProvider { /** * If there's an ongoing sync for a certain identifier return it. * - * @param {number} id Unique sync identifier per component. + * @param {string | number} id Unique sync identifier per component. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise of the current sync or undefined if there isn't any. */ - getOngoingSync(id: number, siteId?: string): Promise { + getOngoingSync(id: string | number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); if (this.isSyncing(id, siteId)) { @@ -81,59 +94,55 @@ export class CoreSyncBaseProvider { /** * Get the synchronization time. Returns 0 if no time stored. * - * @param {number} id Unique sync identifier per component. + * @param {string | number} id Unique sync identifier per component. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the time. */ - getSyncTime(id: number, siteId?: string): Promise { - return this.sitesProvider.getSiteDb(siteId).then((db) => { - return db.getRecord(CoreSyncProvider.SYNC_TABLE, { component: this.component, id: id }).then((entry) => { - return entry.time; - }).catch(() => { - return 0; - }); + getSyncTime(id: string | number, siteId?: string): Promise { + return this.syncProvider.getSyncRecord(this.component, id, siteId).then((entry) => { + return entry.time; + }).catch(() => { + return 0; }); } /** * Get the synchronization warnings of an instance. * - * @param {number} id Unique sync identifier per component. + * @param {string | number} id Unique sync identifier per component. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the warnings. */ - getSyncWarnings(id: number, siteId?: string): Promise { - return this.sitesProvider.getSiteDb(siteId).then((db) => { - return db.getRecord(CoreSyncProvider.SYNC_TABLE, { component: this.component, id: id }).then((entry) => { - try { - return JSON.parse(entry.warnings); - } catch (ex) { - return []; - } - }).catch(() => { + getSyncWarnings(id: string | number, siteId?: string): Promise { + return this.syncProvider.getSyncRecord(this.component, id, siteId).then((entry) => { + try { + return JSON.parse(entry.warnings); + } catch (ex) { return []; - }); + } + }).catch(() => { + return []; }); } /** * Create a unique identifier from component and id. * - * @param {number} id Unique sync identifier per component. + * @param {string | number} id Unique sync identifier per component. * @return {string} Unique identifier from component and id. */ - protected getUniqueSyncId(id: number): string { + protected getUniqueSyncId(id: string | number): string { return this.component + '#' + id; } /** * Check if a there's an ongoing syncronization for the given id. * - * @param {number} id Unique sync identifier per component. + * @param {string | number} id Unique sync identifier per component. * @param {string} [siteId] Site ID. If not defined, current site. * @return {boolean} Whether it's synchronizing. */ - isSyncing(id: number, siteId?: string): boolean { + isSyncing(id: string | number, siteId?: string): boolean { siteId = siteId || this.sitesProvider.getCurrentSiteId(); const uniqueId = this.getUniqueSyncId(id); @@ -144,11 +153,11 @@ export class CoreSyncBaseProvider { /** * Check if a sync is needed: if a certain time has passed since the last time. * - * @param {number} id Unique sync identifier per component. + * @param {string} id Unique sync identifier per component. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with boolean: whether sync is needed. */ - isSyncNeeded(id: number, siteId?: string): Promise { + isSyncNeeded(id: string, siteId?: string): Promise { return this.getSyncTime(id, siteId).then((time) => { return Date.now() - this.syncInterval >= time; }); @@ -157,33 +166,67 @@ export class CoreSyncBaseProvider { /** * Set the synchronization time. * - * @param {number} id Unique sync identifier per component. + * @param {string | number} id Unique sync identifier per component. * @param {string} [siteId] Site ID. If not defined, current site. * @param {number} [time] Time to set. If not defined, current time. * @return {Promise} Promise resolved when the time is set. */ - setSyncTime(id: number, siteId?: string, time?: number): Promise { - return this.sitesProvider.getSiteDb(siteId).then((db) => { - time = typeof time != 'undefined' ? time : Date.now(); + setSyncTime(id: string | number, siteId?: string, time?: number): Promise { + time = typeof time != 'undefined' ? time : Date.now(); - return db.insertOrUpdateRecord(CoreSyncProvider.SYNC_TABLE, { time: time }, { component: this.component, id: id }); - }); + return this.syncProvider.insertOrUpdateSyncRecord(this.component, id, { time: time }, siteId); } /** * Set the synchronization warnings. * - * @param {number} id Unique sync identifier per component. + * @param {string} id Unique sync identifier per component. * @param {string[]} warnings Warnings to set. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when done. */ - setSyncWarnings(id: number, warnings: string[], siteId?: string): Promise { - return this.sitesProvider.getSiteDb(siteId).then((db) => { - warnings = warnings || []; + setSyncWarnings(id: string, warnings: string[], siteId?: string): Promise { + const warningsText = JSON.stringify(warnings || []); - return db.insertOrUpdateRecord(CoreSyncProvider.SYNC_TABLE, { warnings: JSON.stringify(warnings) }, - { component: this.component, id: id }); + return this.syncProvider.insertOrUpdateSyncRecord(this.component, id, { warnings: warningsText }, siteId); + } + + /** + * Execute a sync function on selected sites. + * + * @param {string} syncFunctionLog Log message to explain the sync function purpose. + * @param {Function} syncFunction Sync function to execute. + * @param {any[]} [params] Array that defines the params that admit the funcion. + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @return {Promise} Resolved with siteIds selected. Rejected if offline. + */ + syncOnSites(syncFunctionLog: string, syncFunction: Function, params?: any[], siteId?: string): Promise { + if (!this.appProvider.isOnline()) { + this.logger.debug(`Cannot sync '${syncFunctionLog}' because device is offline.`); + + return Promise.reject(null); + } + + let promise; + if (!siteId) { + // No site ID defined, sync all sites. + this.logger.debug(`Try to sync '${syncFunctionLog}' in all sites.`); + promise = this.sitesProvider.getSitesIds(); + } else { + this.logger.debug(`Try to sync '${syncFunctionLog}' in site '${siteId}'.`); + promise = Promise.resolve([siteId]); + } + + params = params || []; + + return promise.then((siteIds) => { + const sitePromises = []; + siteIds.forEach((siteId) => { + // Execute function for every site selected. + sitePromises.push(syncFunction.apply(syncFunction, [siteId].concat(params))); + }); + + return Promise.all(sitePromises); }); } @@ -191,11 +234,11 @@ export class CoreSyncBaseProvider { * If there's an ongoing sync for a certain identifier, wait for it to end. * If there's no sync ongoing the promise will be resolved right away. * - * @param {number} id Unique sync identifier per component. + * @param {string | number} id Unique sync identifier per component. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when there's no sync going on for the identifier. */ - waitForSync(id: number, siteId?: string): Promise { + waitForSync(id: string | number, siteId?: string): Promise { const promise = this.getOngoingSync(id, siteId); if (promise) { return promise.catch(() => { @@ -204,5 +247,5 @@ export class CoreSyncBaseProvider { } return Promise.resolve(); - } +} } diff --git a/src/classes/delegate.ts b/src/classes/delegate.ts index b26d26a32..00e1bf791 100644 --- a/src/classes/delegate.ts +++ b/src/classes/delegate.ts @@ -13,9 +13,9 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreLoggerProvider } from '../providers/logger'; -import { CoreSitesProvider } from '../providers/sites'; -import { CoreEventsProvider } from '../providers/events'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreEventsProvider } from '@providers/events'; export interface CoreDelegateHandler { /** diff --git a/src/classes/site.ts b/src/classes/site.ts index e5e280cb3..3c3480f25 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -16,18 +16,18 @@ import { Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { HttpClient } from '@angular/common/http'; import { SQLiteDB } from './sqlitedb'; -import { CoreAppProvider } from '../providers/app'; -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 { CoreDomUtilsProvider } from '../providers/utils/dom'; -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 { CoreConstants } from '../core/constants'; +import { CoreAppProvider } from '@providers/app'; +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 { CoreDomUtilsProvider } from '@providers/utils/dom'; +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 { CoreConstants } from '@core/constants'; import { CoreConfigConstants } from '../configconstants'; import { Md5 } from 'ts-md5/dist/md5'; import { InAppBrowserObject } from '@ionic-native/in-app-browser'; @@ -531,7 +531,7 @@ export class CoreSite { } else { this.logger.error(`WS function '${method}' is not available, even in compatibility mode.`); - return Promise.reject(this.wsProvider.createFakeWSError('core.wsfunctionnotavailable', true)); + return Promise.reject(this.utils.createFakeWSError('core.wsfunctionnotavailable', true)); } } @@ -560,7 +560,7 @@ export class CoreSite { data = this.wsProvider.convertValuesToString(data, wsPreSets.cleanUnicode); } catch (e) { // Empty cleaned text found. - return Promise.reject(this.wsProvider.createFakeWSError('core.unicodenotsupportedcleanerror', true)); + return Promise.reject(this.utils.createFakeWSError('core.unicodenotsupportedcleanerror', true)); } return this.getFromCache(method, data, preSets).catch(() => { diff --git a/src/components/components.module.ts b/src/components/components.module.ts index d1dddb4fb..0c4a8b7de 100644 --- a/src/components/components.module.ts +++ b/src/components/components.module.ts @@ -15,8 +15,8 @@ import { NgModule } from '@angular/core'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreDirectivesModule } from '../directives/directives.module'; -import { CorePipesModule } from '../pipes/pipes.module'; +import { CoreDirectivesModule } from '@directives'; +import { CorePipesModule } from '@pipes'; import { CoreLoadingComponent } from './loading/loading'; import { CoreMarkRequiredComponent } from './mark-required/mark-required'; import { CoreInputErrorsComponent } from './input-errors/input-errors'; @@ -39,6 +39,7 @@ import { CoreTabComponent } from './tabs/tab'; import { CoreRichTextEditorComponent } from './rich-text-editor/rich-text-editor'; import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons'; import { CoreDynamicComponent } from './dynamic-component/dynamic-component'; +import { CoreSendMessageFormComponent } from './send-message-form/send-message-form'; @NgModule({ declarations: [ @@ -63,7 +64,8 @@ import { CoreDynamicComponent } from './dynamic-component/dynamic-component'; CoreTabComponent, CoreRichTextEditorComponent, CoreNavBarButtonsComponent, - CoreDynamicComponent + CoreDynamicComponent, + CoreSendMessageFormComponent ], entryComponents: [ CoreContextMenuPopoverComponent, @@ -95,7 +97,8 @@ import { CoreDynamicComponent } from './dynamic-component/dynamic-component'; CoreTabComponent, CoreRichTextEditorComponent, CoreNavBarButtonsComponent, - CoreDynamicComponent + CoreDynamicComponent, + CoreSendMessageFormComponent ] }) export class CoreComponentsModule {} diff --git a/src/components/context-menu/context-menu-popover.ts b/src/components/context-menu/context-menu-popover.ts index 0dfc26b31..1d80f29be 100644 --- a/src/components/context-menu/context-menu-popover.ts +++ b/src/components/context-menu/context-menu-popover.ts @@ -15,7 +15,7 @@ import { Component } from '@angular/core'; import { NavParams, ViewController } from 'ionic-angular'; import { CoreContextMenuItemComponent } from './context-menu-item'; -import { CoreLoggerProvider } from '../../providers/logger'; +import { CoreLoggerProvider } from '@providers/logger'; /** * Component to display a list of items received by param in a popover. diff --git a/src/components/context-menu/context-menu.ts b/src/components/context-menu/context-menu.ts index 6ef697cc4..335aa2932 100644 --- a/src/components/context-menu/context-menu.ts +++ b/src/components/context-menu/context-menu.ts @@ -15,7 +15,7 @@ import { Component, Input, OnInit, OnDestroy, ElementRef } from '@angular/core'; import { PopoverController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreDomUtilsProvider } from '../../providers/utils/dom'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreContextMenuItemComponent } from './context-menu-item'; import { CoreContextMenuPopoverComponent } from './context-menu-popover'; import { Subject } from 'rxjs'; @@ -34,6 +34,7 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { hideMenu: boolean; ariaLabel: string; protected items: CoreContextMenuItemComponent[] = []; + protected itemsMovedToParent: CoreContextMenuItemComponent[] = []; protected itemsChangedStream: Subject; // Stream to update the hideMenu boolean when items change. protected instanceId: string; protected parentContextMenu: CoreContextMenuComponent; @@ -74,7 +75,11 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { if (this.parentContextMenu) { // All items were moved to the "parent" menu. Add the item in there. this.parentContextMenu.addItem(item); - } else { + + if (this.itemsMovedToParent.indexOf(item) == -1) { + this.itemsMovedToParent.push(item); + } + } else if (this.items.indexOf(item) == -1) { this.items.push(item); this.itemsChanged(); } @@ -103,7 +108,9 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { // Add all the items to the other menu. for (let i = 0; i < this.items.length; i++) { - contextMenu.addItem(this.items[i]); + const item = this.items[i]; + contextMenu.addItem(item); + this.itemsMovedToParent.push(item); } // Remove all items from the current menu. @@ -120,6 +127,11 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { if (this.parentContextMenu) { // All items were moved to the "parent" menu. Remove the item from there. this.parentContextMenu.removeItem(item); + + const index = this.itemsMovedToParent.indexOf(item); + if (index >= 0) { + this.itemsMovedToParent.splice(index, 1); + } } else { const index = this.items.indexOf(item); if (index >= 0) { @@ -129,6 +141,28 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { } } + /** + * Remove the items that were merged to a parent context menu. + */ + removeMergedItems(): void { + if (this.parentContextMenu) { + for (let i = 0; i < this.itemsMovedToParent.length; i++) { + this.parentContextMenu.removeItem(this.itemsMovedToParent[i]); + } + } + } + + /** + * Restore the items that were merged to a parent context menu. + */ + restoreMergedItems(): void { + if (this.parentContextMenu) { + for (let i = 0; i < this.itemsMovedToParent.length; i++) { + this.parentContextMenu.addItem(this.itemsMovedToParent[i]); + } + } + } + /** * Show the context menu. * @@ -146,5 +180,6 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.domUtils.removeInstanceById(this.instanceId); + this.removeMergedItems(); } } diff --git a/src/components/dynamic-component/dynamic-component.ts b/src/components/dynamic-component/dynamic-component.ts index 2c8e5898b..f2a045043 100644 --- a/src/components/dynamic-component/dynamic-component.ts +++ b/src/components/dynamic-component/dynamic-component.ts @@ -16,7 +16,7 @@ import { Component, Input, ViewChild, OnInit, OnChanges, DoCheck, ViewContainerRef, ComponentFactoryResolver, KeyValueDiffers, SimpleChange } from '@angular/core'; -import { CoreLoggerProvider } from '../../providers/logger'; +import { CoreLoggerProvider } from '@providers/logger'; /** * Component to create another component dynamically. diff --git a/src/components/file/file.ts b/src/components/file/file.ts index 60825b2f8..20023d454 100644 --- a/src/components/file/file.ts +++ b/src/components/file/file.ts @@ -14,15 +14,15 @@ import { Component, Input, Output, OnInit, OnDestroy, EventEmitter } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { CoreAppProvider } from '../../providers/app'; -import { CoreEventsProvider } from '../../providers/events'; -import { CoreFileProvider } from '../../providers/file'; -import { CoreFilepoolProvider } from '../../providers/filepool'; -import { CoreSitesProvider } from '../../providers/sites'; -import { CoreDomUtilsProvider } from '../../providers/utils/dom'; -import { CoreMimetypeUtilsProvider } from '../../providers/utils/mimetype'; -import { CoreUtilsProvider } from '../../providers/utils/utils'; -import { CoreConstants } from '../../core/constants'; +import { CoreAppProvider } from '@providers/app'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreFileProvider } from '@providers/file'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreConstants } from '@core/constants'; /** * Component to handle a remote file. Shows the file name, icon (depending on mimetype) and a button diff --git a/src/components/iframe/iframe.ts b/src/components/iframe/iframe.ts index 01686ac9e..005ec72a9 100644 --- a/src/components/iframe/iframe.ts +++ b/src/components/iframe/iframe.ts @@ -15,13 +15,13 @@ import { Component, Input, OnInit, ViewChild, ElementRef } from '@angular/core'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { Platform } from 'ionic-angular'; -import { CoreFileProvider } from '../../providers/file'; -import { CoreLoggerProvider } from '../../providers/logger'; -import { CoreSitesProvider } from '../../providers/sites'; -import { CoreDomUtilsProvider } from '../../providers/utils/dom'; -import { CoreTextUtilsProvider } from '../../providers/utils/text'; -import { CoreUrlUtilsProvider } from '../../providers/utils/url'; -import { CoreUtilsProvider } from '../../providers/utils/utils'; +import { CoreFileProvider } from '@providers/file'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; +import { CoreUtilsProvider } from '@providers/utils/utils'; /** */ diff --git a/src/components/loading/loading.html b/src/components/loading/loading.html index d6a224a91..c7fcb6833 100644 --- a/src/components/loading/loading.html +++ b/src/components/loading/loading.html @@ -1,9 +1,8 @@ - -
+

{{message}}

- + \ No newline at end of file diff --git a/src/components/loading/loading.scss b/src/components/loading/loading.scss index 2fb7b23f1..8dce40229 100644 --- a/src/components/loading/loading.scss +++ b/src/components/loading/loading.scss @@ -13,6 +13,7 @@ core-loading { &.core-loading-noheight .core-loading-content { height: auto; } + @include core-transition(core-show-animation); } .scroll-content > core-loading > .core-loading-container, diff --git a/src/components/loading/loading.ts b/src/components/loading/loading.ts index 624c5a825..e04326540 100644 --- a/src/components/loading/loading.ts +++ b/src/components/loading/loading.ts @@ -14,6 +14,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; +import { coreShowHideAnimation } from '@classes/animations'; /** * Component to show a loading spinner and message while data is being loaded. @@ -37,7 +38,8 @@ import { TranslateService } from '@ngx-translate/core'; */ @Component({ selector: 'core-loading', - templateUrl: 'loading.html' + templateUrl: 'loading.html', + animations: [coreShowHideAnimation] }) export class CoreLoadingComponent implements OnInit { @Input() hideUntil: boolean; // Determine when should the contents be shown. diff --git a/src/components/local-file/local-file.ts b/src/components/local-file/local-file.ts index f2573959b..6f7dbb871 100644 --- a/src/components/local-file/local-file.ts +++ b/src/components/local-file/local-file.ts @@ -14,11 +14,11 @@ import { Component, Input, Output, OnInit, EventEmitter } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { CoreFileProvider } from '../../providers/file'; -import { CoreDomUtilsProvider } from '../../providers/utils/dom'; -import { CoreMimetypeUtilsProvider } from '../../providers/utils/mimetype'; -import { CoreTextUtilsProvider } from '../../providers/utils/text'; -import { CoreUtilsProvider } from '../../providers/utils/utils'; +import { CoreFileProvider } from '@providers/file'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import * as moment from 'moment'; /** diff --git a/src/components/mark-required/mark-required.ts b/src/components/mark-required/mark-required.ts index bbc85d34b..c78a1692b 100644 --- a/src/components/mark-required/mark-required.ts +++ b/src/components/mark-required/mark-required.ts @@ -14,8 +14,8 @@ import { Component, Input, OnInit, AfterViewInit, ElementRef } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { CoreTextUtilsProvider } from '../../providers/utils/text'; -import { CoreUtilsProvider } from '../../providers/utils/utils'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; /** * Directive to add a red asterisk for required input fields. diff --git a/src/components/navbar-buttons/navbar-buttons.ts b/src/components/navbar-buttons/navbar-buttons.ts index 3bdb50e26..d682248a6 100644 --- a/src/components/navbar-buttons/navbar-buttons.ts +++ b/src/components/navbar-buttons/navbar-buttons.ts @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnInit, ContentChildren, ElementRef, QueryList } from '@angular/core'; +import { Component, Input, OnInit, OnDestroy, ContentChildren, ElementRef, QueryList } from '@angular/core'; import { Button } from 'ionic-angular'; -import { CoreLoggerProvider } from '../../providers/logger'; -import { CoreDomUtilsProvider } from '../../providers/utils/dom'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreContextMenuComponent } from '../context-menu/context-menu'; /** * Component to add buttons to the app's header without having to place them inside the header itself. This is meant for @@ -26,10 +27,12 @@ import { CoreDomUtilsProvider } from '../../providers/utils/dom'; * * 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. + * * Example usage: * * - * * @@ -38,37 +41,35 @@ import { CoreDomUtilsProvider } from '../../providers/utils/dom'; selector: 'core-navbar-buttons', template: '' }) -export class CoreNavBarButtonsComponent implements OnInit { +export class CoreNavBarButtonsComponent implements OnInit, OnDestroy { protected BUTTON_HIDDEN_CLASS = 'core-navbar-button-hidden'; // If the hidden input is true, hide all buttons. @Input('hidden') set hidden(value: boolean) { this._hidden = value; - if (this._buttons) { - this._buttons.forEach((button: Button) => { - this.showHideButton(button); - }); - } + this.showHideAllElements(); } - // Get all the buttons inside this directive. + // Get all the ion-buttons inside this directive and apply the role bar-button. @ContentChildren(Button) set buttons(buttons: QueryList + diff --git a/src/components/search-box/search-box.ts b/src/components/search-box/search-box.ts index 497de1fb6..2c5110b65 100644 --- a/src/components/search-box/search-box.ts +++ b/src/components/search-box/search-box.ts @@ -14,7 +14,7 @@ import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { CoreUtilsProvider } from '../../providers/utils/utils'; +import { CoreUtilsProvider } from '@providers/utils/utils'; /** * Component to display a "search box". @@ -31,23 +31,30 @@ import { CoreUtilsProvider } from '../../providers/utils/utils'; templateUrl: 'search-box.html' }) export class CoreSearchBoxComponent implements OnInit { - @Input() initialValue? = ''; // Initial value for search text. @Input() searchLabel?: string; // Label to be used on action button. @Input() placeholder?: string; // Placeholder text for search text input. @Input() autocorrect? = 'on'; // Enables/disable Autocorrection on search text input. @Input() spellcheck?: string | boolean = true; // Enables/disable Spellchecker on search text input. @Input() autoFocus?: string | boolean; // Enables/disable Autofocus when entering view. @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. @Output() onSubmit: EventEmitter; // Send data when submitting the search form. + @Output() onClear?: EventEmitter; // Send event when clearing the search form. + + searched = false; + searchText = ''; constructor(private translate: TranslateService, private utils: CoreUtilsProvider) { - this.onSubmit = new EventEmitter(); + this.onSubmit = new EventEmitter(); + this.onClear = new EventEmitter(); } ngOnInit(): void { this.searchLabel = this.searchLabel || this.translate.instant('core.search'); this.placeholder = this.placeholder || this.translate.instant('core.search'); this.spellcheck = this.utils.isTrueOrOne(this.spellcheck); + this.showClear = this.utils.isTrueOrOne(this.showClear); } /** @@ -56,11 +63,21 @@ export class CoreSearchBoxComponent implements OnInit { * @param {string} value Entered value. */ submitForm(value: string): void { - if (value.length < this.lengthCheck) { + if (this.searchText.length < this.lengthCheck) { // The view should handle this case, but we check it here too just in case. return; } - this.onSubmit.emit(value); + this.searched = true; + this.onSubmit.emit(this.searchText); + } + + /** + * Form submitted. + */ + clearForm(): void { + this.searched = false; + this.searchText = ''; + this.onClear.emit(); } } diff --git a/src/components/send-message-form/send-message-form.html b/src/components/send-message-form/send-message-form.html new file mode 100644 index 000000000..8a7aa0ef1 --- /dev/null +++ b/src/components/send-message-form/send-message-form.html @@ -0,0 +1,8 @@ +
+ + + + +
diff --git a/src/components/send-message-form/send-message-form.scss b/src/components/send-message-form/send-message-form.scss new file mode 100644 index 000000000..d6fbdcecb --- /dev/null +++ b/src/components/send-message-form/send-message-form.scss @@ -0,0 +1,36 @@ +$core-send-message-input-background: $gray; +$core-send-message-input-color: $black; + +core-send-message-form { + background: $white; + + form { + position: relative; + display: flex; + align-items: center; + width: 100%; + flex-shrink: 1; + width: 100%; + } + + .core-send-message-input { + @include appearance(none); + display: block; + width: 100%; + border: 0; + font-family: inherit; + align-self: self-start; + background: $core-send-message-input-background; + color: $core-send-message-input-color; + border-radius: 5px; + margin: 0 5px; + } + + .core-send-message-button { + @include margin(0); + @include padding(0); + display: none; + min-height: 0; + align-self: self-end; + } +} \ No newline at end of file diff --git a/src/components/send-message-form/send-message-form.ts b/src/components/send-message-form/send-message-form.ts new file mode 100644 index 000000000..2342d42e5 --- /dev/null +++ b/src/components/send-message-form/send-message-form.ts @@ -0,0 +1,76 @@ +// (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, Output, EventEmitter, OnInit } from '@angular/core'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; + +/** + * Component to display a "send message form". + * + * @description + * This component will display a standalone send message form in order to have a better UX. + * + * Example usage: + * + */ +@Component({ + selector: 'core-send-message-form', + templateUrl: 'send-message-form.html' +}) +export class CoreSendMessageFormComponent implements OnInit { + @Input() placeholder = ''; // Placeholder for the input area. + @Input() showKeyboard = false; // If keyboard is shown or not. + @Output() onSubmit: EventEmitter; // Send data when submitting the message form. + @Output() onResize: EventEmitter; // Emit when resizing the textarea. + + message: string; + + constructor(private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider) { + this.onSubmit = new EventEmitter(); + this.onResize = new EventEmitter(); + } + + ngOnInit(): void { + this.showKeyboard = this.utils.isTrueOrOne(this.showKeyboard); + } + + /** + * Form submitted. + * @param {any} $event Form submit + */ + submitForm($event: any): void { + let value = this.message.trim(); + $event.target.reset(); + + // Focus again on textarea. + $event.target[0].focus(); + + if (!value) { + // Silent error. + return; + } + + value = this.textUtils.replaceNewLines(value, '
'); + this.onSubmit.emit(value); + } + + /** + * Textarea resized. + */ + textareaResized(): void { + this.onResize.emit(); + } +} diff --git a/src/components/show-password/show-password.ts b/src/components/show-password/show-password.ts index 6317be448..9da04c954 100644 --- a/src/components/show-password/show-password.ts +++ b/src/components/show-password/show-password.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, OnInit, AfterViewInit, Input, ElementRef } from '@angular/core'; -import { CoreUtilsProvider } from '../../providers/utils/utils'; +import { CoreUtilsProvider } from '@providers/utils/utils'; /** * Component to allow showing and hiding a password. The affected input MUST have a name to identify it. diff --git a/src/components/site-picker/site-picker.ts b/src/components/site-picker/site-picker.ts index 9562f207a..6611a2190 100644 --- a/src/components/site-picker/site-picker.ts +++ b/src/components/site-picker/site-picker.ts @@ -14,8 +14,8 @@ import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { CoreSitesProvider } from '../../providers/sites'; -import { CoreTextUtilsProvider } from '../../providers/utils/text'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; /** * Component to display a site selector. It will display a select with the list of sites. If the selected site changes, diff --git a/src/components/tabs/tab.ts b/src/components/tabs/tab.ts index 46467c2e6..95102e758 100644 --- a/src/components/tabs/tab.ts +++ b/src/components/tabs/tab.ts @@ -15,6 +15,8 @@ import { Component, Input, Output, OnInit, OnDestroy, ElementRef, EventEmitter, ContentChild, TemplateRef } from '@angular/core'; import { CoreTabsComponent } from './tabs'; import { Content } from 'ionic-angular'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreNavBarButtonsComponent } from '../navbar-buttons/navbar-buttons'; /** * A tab to use inside core-tabs. The content of this tab will be displayed when the tab is selected. @@ -55,7 +57,7 @@ export class CoreTabComponent implements OnInit, OnDestroy { element: HTMLElement; // The core-tab element. loaded = false; - constructor(private tabs: CoreTabsComponent, element: ElementRef) { + constructor(protected tabs: CoreTabsComponent, element: ElementRef, protected domUtils: CoreDomUtilsProvider) { this.element = element.nativeElement; } @@ -81,6 +83,7 @@ export class CoreTabComponent implements OnInit, OnDestroy { this.loaded = true; this.ionSelect.emit(this); + this.showHideNavBarButtons(true); // Setup tab scrolling. setTimeout(() => { @@ -97,5 +100,39 @@ export class CoreTabComponent implements OnInit, OnDestroy { */ unselectTab(): void { this.element.classList.remove('selected'); + this.showHideNavBarButtons(false); + } + + /** + * Get all child core-navbar-buttons. We need to use querySelectorAll because ContentChildren doesn't work with ng-template. + * https://github.com/angular/angular/issues/14842 + * + * @return {CoreNavBarButtonsComponent[]} List of component instances. + */ + protected getChildrenNavBarButtons(): CoreNavBarButtonsComponent[] { + const elements = this.element.querySelectorAll('core-navbar-buttons'), + instances: CoreNavBarButtonsComponent[] = []; + + for (let i = 0; i < elements.length; i++) { + const instance = this.domUtils.getInstanceByElement(elements[i]); + if (instance) { + instances.push(instance); + } + } + + return instances; + } + + /** + * Show all hide all children navbar buttons. + * + * @param {boolean} show Whether to show or hide the buttons. + */ + protected showHideNavBarButtons(show: boolean): void { + const instances = this.getChildrenNavBarButtons(); + + for (const i in instances) { + instances[i].forceHide(!show); + } } } diff --git a/src/components/tabs/tabs.scss b/src/components/tabs/tabs.scss index b7e3916fa..a7e779cba 100644 --- a/src/components/tabs/tabs.scss +++ b/src/components/tabs/tabs.scss @@ -76,4 +76,15 @@ core-tabs { .core-tabs-bar::after { @extend .header-md::after; } -} \ No newline at end of file +} + +.ios, .md, .wp { + .core-avoid-header ion-content core-tabs core-tab ion-content { + top: 0; + height: 100%; + } + ion-content core-tabs core-tab .core-avoid-header ion-content { + top: 0; + height: 100%; + } +} diff --git a/src/core/contentlinks/classes/module-grade-handler.ts b/src/core/contentlinks/classes/module-grade-handler.ts index edf7bcac0..36296e5b6 100644 --- a/src/core/contentlinks/classes/module-grade-handler.ts +++ b/src/core/contentlinks/classes/module-grade-handler.ts @@ -15,8 +15,8 @@ import { NavController } from 'ionic-angular'; import { CoreContentLinksAction } from '../providers/delegate'; import { CoreContentLinksHandlerBase } from './base-handler'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreCourseHelperProvider } from '../../course/providers/helper'; /** diff --git a/src/core/contentlinks/pages/choose-site/choose-site.module.ts b/src/core/contentlinks/pages/choose-site/choose-site.module.ts index abd8544de..10e7ffe90 100644 --- a/src/core/contentlinks/pages/choose-site/choose-site.module.ts +++ b/src/core/contentlinks/pages/choose-site/choose-site.module.ts @@ -16,8 +16,8 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { CoreContentLinksChooseSitePage } from './choose-site'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreComponentsModule } from '../../../../components/components.module'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/core/contentlinks/pages/choose-site/choose-site.ts b/src/core/contentlinks/pages/choose-site/choose-site.ts index 922e743ea..64bfceead 100644 --- a/src/core/contentlinks/pages/choose-site/choose-site.ts +++ b/src/core/contentlinks/pages/choose-site/choose-site.ts @@ -14,8 +14,8 @@ import { Component, OnInit } from '@angular/core'; import { IonicPage, NavController, NavParams } from 'ionic-angular'; -import { CoreSitesProvider } from '../../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreContentLinksDelegate, CoreContentLinksAction } from '../../providers/delegate'; import { CoreContentLinksHelperProvider } from '../../providers/helper'; diff --git a/src/core/contentlinks/providers/delegate.ts b/src/core/contentlinks/providers/delegate.ts index 5aecc1f5a..21b1da364 100644 --- a/src/core/contentlinks/providers/delegate.ts +++ b/src/core/contentlinks/providers/delegate.ts @@ -14,10 +14,10 @@ import { Injectable } from '@angular/core'; import { NavController } from 'ionic-angular'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreUrlUtilsProvider } from '../../../providers/utils/url'; -import { CoreUtilsProvider } from '../../../providers/utils/utils'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; +import { CoreUtilsProvider } from '@providers/utils/utils'; /** * Interface that all handlers must implement. diff --git a/src/core/contentlinks/providers/helper.ts b/src/core/contentlinks/providers/helper.ts index cdf1be398..c160a86b1 100644 --- a/src/core/contentlinks/providers/helper.ts +++ b/src/core/contentlinks/providers/helper.ts @@ -15,14 +15,14 @@ import { Injectable } from '@angular/core'; import { NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreAppProvider } from '../../../providers/app'; -import { CoreEventsProvider } from '../../../providers/events'; -import { CoreInitDelegate } from '../../../providers/init'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; -import { CoreTextUtilsProvider } from '../../../providers/utils/text'; -import { CoreUrlUtilsProvider } from '../../../providers/utils/url'; +import { CoreAppProvider } from '@providers/app'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreInitDelegate } from '@providers/init'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreLoginHelperProvider } from '../../login/providers/helper'; import { CoreContentLinksDelegate, CoreContentLinksAction } from './delegate'; import { CoreConstants } from '../../constants'; diff --git a/src/core/course/classes/module-prefetch-handler.ts b/src/core/course/classes/module-prefetch-handler.ts index 47b115140..7fde10f08 100644 --- a/src/core/course/classes/module-prefetch-handler.ts +++ b/src/core/course/classes/module-prefetch-handler.ts @@ -14,11 +14,11 @@ import { Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { CoreAppProvider } from '../../../providers/app'; -import { CoreFilepoolProvider } from '../../../providers/filepool'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; -import { CoreUtilsProvider } from '../../../providers/utils/utils'; +import { CoreAppProvider } from '@providers/app'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '../providers/course'; import { CoreCourseModulePrefetchHandler } from '../providers/module-prefetch-delegate'; import { CoreConstants } from '../../constants'; diff --git a/src/core/course/components/components.module.ts b/src/core/course/components/components.module.ts index ab679c5f3..021427bf3 100644 --- a/src/core/course/components/components.module.ts +++ b/src/core/course/components/components.module.ts @@ -16,8 +16,8 @@ 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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; import { CoreCourseFormatComponent } from './format/format'; import { CoreCourseModuleComponent } from './module/module'; import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion'; diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts index ceb8780ca..b4a2985a9 100644 --- a/src/core/course/components/format/format.ts +++ b/src/core/course/components/format/format.ts @@ -17,14 +17,14 @@ import { } from '@angular/core'; import { Content } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreEventsProvider } from '../../../../providers/events'; -import { CoreSitesProvider } from '../../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreCourseProvider } from '../../../course/providers/course'; import { CoreCourseHelperProvider } from '../../../course/providers/helper'; import { CoreCourseFormatDelegate } from '../../../course/providers/format-delegate'; import { CoreCourseModulePrefetchDelegate } from '../../../course/providers/module-prefetch-delegate'; -import { CoreDynamicComponent } from '../../../../components/dynamic-component/dynamic-component'; +import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; /** * Component to display course contents using a certain format. If the format isn't found, use default one. diff --git a/src/core/course/components/module-completion/module-completion.ts b/src/core/course/components/module-completion/module-completion.ts index 56b910978..6290ae798 100644 --- a/src/core/course/components/module-completion/module-completion.ts +++ b/src/core/course/components/module-completion/module-completion.ts @@ -14,9 +14,9 @@ import { Component, Input, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { CoreSitesProvider } from '../../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; -import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUserProvider } from '../../../user/providers/user'; /** diff --git a/src/core/course/components/module/module.scss b/src/core/course/components/module/module.scss index c76d9e6f1..8a15d7dd8 100644 --- a/src/core/course/components/module/module.scss +++ b/src/core/course/components/module/module.scss @@ -43,7 +43,7 @@ core-course-module { pointer-events: auto; } - .core-module-buttons-more .spinner { + .core-module-buttons-more .spinner { right: 13px; position: absolute; } diff --git a/src/core/course/components/module/module.ts b/src/core/course/components/module/module.ts index 15a66286b..00d0bbf1f 100644 --- a/src/core/course/components/module/module.ts +++ b/src/core/course/components/module/module.ts @@ -14,9 +14,9 @@ import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core'; import { NavController } from 'ionic-angular'; -import { CoreEventsProvider } from '../../../../providers/events'; -import { CoreSitesProvider } from '../../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreCourseHelperProvider } from '../../providers/helper'; import { CoreCourseModuleHandlerButton } from '../../providers/module-delegate'; import { CoreCourseModulePrefetchDelegate, CoreCourseModulePrefetchHandler } from '../../providers/module-prefetch-delegate'; diff --git a/src/core/course/formats/singleactivity/singleactivity.module.ts b/src/core/course/formats/singleactivity/singleactivity.module.ts index 8899f01db..a35d4ad93 100644 --- a/src/core/course/formats/singleactivity/singleactivity.module.ts +++ b/src/core/course/formats/singleactivity/singleactivity.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { CoreCourseFormatSingleActivityComponent } from './components/singleactivity'; import { CoreCourseFormatSingleActivityHandler } from './providers/handler'; import { CoreCourseFormatDelegate } from '../../providers/format-delegate'; -import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreComponentsModule } from '@components/components.module'; @NgModule({ declarations: [ diff --git a/src/core/course/formats/weeks/providers/handler.ts b/src/core/course/formats/weeks/providers/handler.ts index 6feca32cd..7fea18e9a 100644 --- a/src/core/course/formats/weeks/providers/handler.ts +++ b/src/core/course/formats/weeks/providers/handler.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreCourseFormatHandler } from '../../../providers/format-delegate'; -import { CoreTimeUtilsProvider } from '../../../../../providers/utils/time'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreConstants } from '../../../../constants'; /** diff --git a/src/core/course/pages/section/section.html b/src/core/course/pages/section/section.html index 8420d3fb3..83bf61d17 100644 --- a/src/core/course/pages/section/section.html +++ b/src/core/course/pages/section/section.html @@ -2,12 +2,7 @@ - - - - - - + @@ -15,6 +10,12 @@ + + + + + + diff --git a/src/core/course/pages/section/section.module.ts b/src/core/course/pages/section/section.module.ts index c9ab2da0f..bfa4d3eb3 100644 --- a/src/core/course/pages/section/section.module.ts +++ b/src/core/course/pages/section/section.module.ts @@ -16,8 +16,8 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreCourseSectionPage } from './section'; -import { CoreComponentsModule } from '../../../../components/components.module'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; import { CoreCourseComponentsModule } from '../../components/components.module'; @NgModule({ diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index d0cd1647c..0e0ced8b7 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -15,10 +15,10 @@ import { Component, ViewChild, OnDestroy } from '@angular/core'; import { IonicPage, NavParams, Content, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreEventsProvider } from '../../../../providers/events'; -import { CoreSitesProvider } from '../../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; -import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreCourseProvider } from '../../providers/course'; import { CoreCourseHelperProvider } from '../../providers/helper'; import { CoreCourseFormatDelegate } from '../../providers/format-delegate'; diff --git a/src/core/course/pages/unsupported-module/unsupported-module.module.ts b/src/core/course/pages/unsupported-module/unsupported-module.module.ts index d45906da4..5d2ca24a9 100644 --- a/src/core/course/pages/unsupported-module/unsupported-module.module.ts +++ b/src/core/course/pages/unsupported-module/unsupported-module.module.ts @@ -16,8 +16,8 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreCourseUnsupportedModulePage } from './unsupported-module'; -import { CoreComponentsModule } from '../../../../components/components.module'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; import { CoreCourseComponentsModule } from '../../components/components.module'; @NgModule({ diff --git a/src/core/course/pages/unsupported-module/unsupported-module.ts b/src/core/course/pages/unsupported-module/unsupported-module.ts index 7e8554f59..e52c44b1f 100644 --- a/src/core/course/pages/unsupported-module/unsupported-module.ts +++ b/src/core/course/pages/unsupported-module/unsupported-module.ts @@ -15,7 +15,7 @@ import { Component } from '@angular/core'; import { IonicPage, NavParams } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; /** * Page that displays info about an unsupported module. diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index b814689d7..9239b8921 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -14,12 +14,12 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { CoreEventsProvider } from '../../../providers/events'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreTimeUtilsProvider } from '../../../providers/utils/time'; -import { CoreUtilsProvider } from '../../../providers/utils/utils'; -import { CoreSiteWSPreSets } from '../../../classes/site'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreSiteWSPreSets } from '@classes/site'; import { CoreConstants } from '../../constants'; /** diff --git a/src/core/course/providers/format-delegate.ts b/src/core/course/providers/format-delegate.ts index 48d43bd34..2398f4192 100644 --- a/src/core/course/providers/format-delegate.ts +++ b/src/core/course/providers/format-delegate.ts @@ -14,12 +14,12 @@ import { Injectable } from '@angular/core'; import { NavController } from 'ionic-angular'; -import { CoreEventsProvider } from '../../../providers/events'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; import { CoreCourseProvider } from './course'; import { CoreCourseFormatDefaultHandler } from './default-format'; -import { CoreDelegate, CoreDelegateHandler } from '../../../classes/delegate'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; /** * Interface that all course format handlers must implement. diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index 36fbc2433..fedffa140 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -15,13 +15,13 @@ import { Injectable } from '@angular/core'; import { NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreEventsProvider } from '../../../providers/events'; -import { CoreFilepoolProvider } from '../../../providers/filepool'; -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 { CoreEventsProvider } from '@providers/events'; +import { CoreFilepoolProvider } from '@providers/filepool'; +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 { CoreCourseOptionsDelegate, CoreCourseOptionsHandlerToDisplay } from './options-delegate'; import { CoreSiteHomeProvider } from '../../sitehome/providers/sitehome'; import { CoreCourseProvider } from './course'; @@ -29,7 +29,7 @@ import { CoreCourseModuleDelegate } from './module-delegate'; import { CoreCourseModulePrefetchDelegate } from './module-prefetch-delegate'; import { CoreLoginHelperProvider } from '../../login/providers/helper'; import { CoreConstants } from '../../constants'; -import { CoreSite } from '../../../classes/site'; +import { CoreSite } from '@classes/site'; import * as moment from 'moment'; /** diff --git a/src/core/course/providers/module-delegate.ts b/src/core/course/providers/module-delegate.ts index 1a2e8c057..53f910b08 100644 --- a/src/core/course/providers/module-delegate.ts +++ b/src/core/course/providers/module-delegate.ts @@ -14,12 +14,12 @@ import { Injectable } from '@angular/core'; import { NavController, NavOptions } from 'ionic-angular'; -import { CoreEventsProvider } from '../../../providers/events'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; import { CoreCourseProvider } from './course'; -import { CoreSite } from '../../../classes/site'; -import { CoreDelegate, CoreDelegateHandler } from '../../../classes/delegate'; +import { CoreSite } from '@classes/site'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; /** * Interface that all course module handlers must implement. diff --git a/src/core/course/providers/module-prefetch-delegate.ts b/src/core/course/providers/module-prefetch-delegate.ts index a80e9c74f..76589a93e 100644 --- a/src/core/course/providers/module-prefetch-delegate.ts +++ b/src/core/course/providers/module-prefetch-delegate.ts @@ -13,20 +13,20 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreEventsProvider } from '../../../providers/events'; -import { CoreFileProvider } from '../../../providers/file'; -import { CoreFilepoolProvider } from '../../../providers/filepool'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreTimeUtilsProvider } from '../../../providers/utils/time'; -import { CoreUtilsProvider } from '../../../providers/utils/utils'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreFileProvider } from '@providers/file'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from './course'; -import { CoreCache } from '../../../classes/cache'; -import { CoreSiteWSPreSets } from '../../../classes/site'; +import { CoreCache } from '@classes/cache'; +import { CoreSiteWSPreSets } from '@classes/site'; import { CoreConstants } from '../../constants'; import { Md5 } from 'ts-md5/dist/md5'; import { Subject, BehaviorSubject, Subscription } from 'rxjs'; -import { CoreDelegate, CoreDelegateHandler } from '../../../classes/delegate'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; /** * Progress of downloading a list of modules. @@ -238,7 +238,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { * @return {boolean} True if can check updates, false otherwise. */ canCheckUpdates(): boolean { - return this.sitesProvider.getCurrentSite().wsAvailable('core_course_check_updates'); + return this.sitesProvider.wsAvailableInCurrentSite('core_course_check_updates'); } /** diff --git a/src/core/course/providers/options-delegate.ts b/src/core/course/providers/options-delegate.ts index 6cb0e43aa..35182a369 100644 --- a/src/core/course/providers/options-delegate.ts +++ b/src/core/course/providers/options-delegate.ts @@ -13,11 +13,11 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreDelegate, CoreDelegateHandler } from '../../../classes/delegate'; -import { CoreEventsProvider } from '../../../providers/events'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreUtilsProvider, PromiseDefer } from '../../../providers/utils/utils'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUtilsProvider, PromiseDefer } from '@providers/utils/utils'; import { CoreCoursesProvider } from '../../courses/providers/courses'; import { CoreCourseProvider } from './course'; diff --git a/src/core/courses/components/components.module.ts b/src/core/courses/components/components.module.ts index d34f17cac..d8dbe5a35 100644 --- a/src/core/courses/components/components.module.ts +++ b/src/core/courses/components/components.module.ts @@ -16,9 +16,9 @@ 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 { CorePipesModule } from '../../../pipes/pipes.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; +import { CorePipesModule } from '@pipes'; import { CoreCoursesCourseProgressComponent } from '../components/course-progress/course-progress'; import { CoreCoursesCourseListItemComponent } from '../components/course-list-item/course-list-item'; import { CoreCoursesOverviewEventsComponent } from '../components/overview-events/overview-events'; diff --git a/src/core/courses/components/course-progress/course-progress.html b/src/core/courses/components/course-progress/course-progress.html index 6eb3c2cd5..ec1484c21 100644 --- a/src/core/courses/components/course-progress/course-progress.html +++ b/src/core/courses/components/course-progress/course-progress.html @@ -1,22 +1,20 @@ - -
-
- - - - -
+ +
diff --git a/src/core/courses/pages/my-overview/my-overview.module.ts b/src/core/courses/pages/my-overview/my-overview.module.ts index 041267ef4..8825f5ab0 100644 --- a/src/core/courses/pages/my-overview/my-overview.module.ts +++ b/src/core/courses/pages/my-overview/my-overview.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreCoursesMyOverviewPage } from './my-overview'; -import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreComponentsModule } from '@components/components.module'; import { CoreCoursesComponentsModule } from '../../components/components.module'; import { CoreSiteHomeComponentsModule } from '../../../sitehome/components/components.module'; diff --git a/src/core/courses/pages/my-overview/my-overview.ts b/src/core/courses/pages/my-overview/my-overview.ts index 20c919f51..a90972fb1 100644 --- a/src/core/courses/pages/my-overview/my-overview.ts +++ b/src/core/courses/pages/my-overview/my-overview.ts @@ -14,8 +14,8 @@ import { Component, OnDestroy } from '@angular/core'; import { IonicPage, NavController } from 'ionic-angular'; -import { CoreSitesProvider } from '../../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreCoursesProvider } from '../../providers/courses'; import { CoreCoursesMyOverviewProvider } from '../../providers/my-overview'; import { CoreCourseHelperProvider } from '../../../course/providers/helper'; @@ -212,14 +212,15 @@ export class CoreCoursesMyOverviewPage implements OnDestroy { /** * The filter has changed. * - * @param {string} newValue New filter value. + * @param {any} Received Event. */ - filterChanged(newValue: string): void { + filterChanged(event: any): void { + const newValue = event.target.value && event.target.value.trim().toLowerCase(); if (!newValue || !this.courses[this.courses.selected]) { this.filteredCourses = this.courses[this.courses.selected]; } else { this.filteredCourses = this.courses[this.courses.selected].filter((course) => { - return course.fullname.toLowerCase().indexOf(newValue.toLowerCase()) > -1; + return course.fullname.toLowerCase().indexOf(newValue) > -1; }); } } diff --git a/src/core/courses/pages/search/search.html b/src/core/courses/pages/search/search.html index 4df0a3845..7e1d94fcb 100644 --- a/src/core/courses/pages/search/search.html +++ b/src/core/courses/pages/search/search.html @@ -4,7 +4,7 @@ - +
{{ 'core.courses.totalcoursesearchresults' | translate:{$a: total} }} diff --git a/src/core/courses/pages/search/search.module.ts b/src/core/courses/pages/search/search.module.ts index c74212c21..3527ebcb3 100644 --- a/src/core/courses/pages/search/search.module.ts +++ b/src/core/courses/pages/search/search.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreCoursesSearchPage } from './search'; -import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreComponentsModule } from '@components/components.module'; import { CoreCoursesComponentsModule } from '../../components/components.module'; @NgModule({ diff --git a/src/core/courses/pages/search/search.ts b/src/core/courses/pages/search/search.ts index 696215a1a..43c988d3d 100644 --- a/src/core/courses/pages/search/search.ts +++ b/src/core/courses/pages/search/search.ts @@ -14,7 +14,7 @@ import { Component } from '@angular/core'; import { IonicPage } from 'ionic-angular'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreCoursesProvider } from '../../providers/courses'; /** diff --git a/src/core/courses/pages/self-enrol-password/self-enrol-password.module.ts b/src/core/courses/pages/self-enrol-password/self-enrol-password.module.ts index 80a4f5183..d1aaa6da6 100644 --- a/src/core/courses/pages/self-enrol-password/self-enrol-password.module.ts +++ b/src/core/courses/pages/self-enrol-password/self-enrol-password.module.ts @@ -16,8 +16,8 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { CoreCoursesSelfEnrolPasswordPage } from './self-enrol-password'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreComponentsModule } from '../../../../components/components.module'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/core/courses/providers/course-link-handler.ts b/src/core/courses/providers/course-link-handler.ts index cec8933d3..3dfb1fd39 100644 --- a/src/core/courses/providers/course-link-handler.ts +++ b/src/core/courses/providers/course-link-handler.ts @@ -14,8 +14,8 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; import { CoreLoginHelperProvider } from '../../login/providers/helper'; diff --git a/src/core/courses/providers/courses.ts b/src/core/courses/providers/courses.ts index f71deddfe..3118858dd 100644 --- a/src/core/courses/providers/courses.ts +++ b/src/core/courses/providers/courses.ts @@ -13,9 +13,9 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreSite } from '../../../classes/site'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSite } from '@classes/site'; /** * Service that provides some features regarding lists of courses and categories. @@ -355,7 +355,7 @@ export class CoreCoursesProvider { * @return {boolean} Whether get courses by field is available. */ isGetCoursesByFieldAvailable(): boolean { - return this.sitesProvider.getCurrentSite().wsAvailable('core_course_get_courses_by_field'); + return this.sitesProvider.wsAvailableInCurrentSite('core_course_get_courses_by_field'); } /** diff --git a/src/core/courses/providers/mainmenu-handler.ts b/src/core/courses/providers/mainmenu-handler.ts index da4a035af..6727ff832 100644 --- a/src/core/courses/providers/mainmenu-handler.ts +++ b/src/core/courses/providers/mainmenu-handler.ts @@ -54,7 +54,7 @@ export class CoreCoursesMainMenuHandler implements CoreMainMenuHandler { getDisplayData(): CoreMainMenuHandlerData { if (this.isOverviewEnabled) { return { - icon: 'ionic', + icon: 'home', title: 'core.courses.courseoverview', page: 'CoreCoursesMyOverviewPage', class: 'core-courseoverview-handler' diff --git a/src/core/courses/providers/my-overview.ts b/src/core/courses/providers/my-overview.ts index f188f6d82..1e744da3c 100644 --- a/src/core/courses/providers/my-overview.ts +++ b/src/core/courses/providers/my-overview.ts @@ -13,8 +13,8 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreSite } from '../../../classes/site'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSite } from '@classes/site'; import * as moment from 'moment'; /** diff --git a/src/core/emulator/classes/inappbrowserobject.ts b/src/core/emulator/classes/inappbrowserobject.ts index f13df5d8c..b557003f7 100644 --- a/src/core/emulator/classes/inappbrowserobject.ts +++ b/src/core/emulator/classes/inappbrowserobject.ts @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreAppProvider } from '../../../providers/app'; -import { CoreFileProvider } from '../../../providers/file'; -import { CoreUrlUtilsProvider } from '../../../providers/utils/url'; +import { CoreAppProvider } from '@providers/app'; +import { CoreFileProvider } from '@providers/file'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { Observable, Observer } from 'rxjs'; import { InAppBrowserObject, InAppBrowserEvent } from '@ionic-native/in-app-browser'; diff --git a/src/core/emulator/classes/sqlitedb.ts b/src/core/emulator/classes/sqlitedb.ts index 60e72126b..90d5654d2 100644 --- a/src/core/emulator/classes/sqlitedb.ts +++ b/src/core/emulator/classes/sqlitedb.ts @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { SQLiteDB } from '../../../classes/sqlitedb'; +/* tslint:disable:no-console */ + +import { SQLiteDB } from '@classes/sqlitedb'; /** * Class to mock the interaction with the SQLite database. @@ -93,6 +95,7 @@ export class SQLiteDBMock extends SQLiteDB { tx.executeSql(sql, params, (tx, results) => { resolve(results); }, (tx, error) => { + console.error(sql, params, error); reject(error); }); }); diff --git a/src/core/emulator/emulator.module.ts b/src/core/emulator/emulator.module.ts index 0437615a4..353369710 100644 --- a/src/core/emulator/emulator.module.ts +++ b/src/core/emulator/emulator.module.ts @@ -16,8 +16,10 @@ import { NgModule } from '@angular/core'; import { Platform } from 'ionic-angular'; // Ionic Native services. +import { Badge } from '@ionic-native/badge'; import { Camera } from '@ionic-native/camera'; import { Clipboard } from '@ionic-native/clipboard'; +import { Device } from '@ionic-native/device'; import { File } from '@ionic-native/file'; import { FileTransfer } from '@ionic-native/file-transfer'; import { Globalization } from '@ionic-native/globalization'; @@ -26,6 +28,7 @@ import { Keyboard } from '@ionic-native/keyboard'; import { LocalNotifications } from '@ionic-native/local-notifications'; import { MediaCapture } from '@ionic-native/media-capture'; import { Network } from '@ionic-native/network'; +import { Push } from '@ionic-native/push'; import { SplashScreen } from '@ionic-native/splash-screen'; import { StatusBar } from '@ionic-native/status-bar'; import { SQLite } from '@ionic-native/sqlite'; @@ -45,13 +48,13 @@ import { ZipMock } from './providers/zip'; import { CoreEmulatorHelperProvider } from './providers/helper'; import { CoreEmulatorCaptureHelperProvider } from './providers/capture-helper'; -import { CoreAppProvider } from '../../providers/app'; -import { CoreFileProvider } from '../../providers/file'; -import { CoreTextUtilsProvider } from '../../providers/utils/text'; -import { CoreMimetypeUtilsProvider } from '../../providers/utils/mimetype'; -import { CoreUrlUtilsProvider } from '../../providers/utils/url'; -import { CoreUtilsProvider } from '../../providers/utils/utils'; -import { CoreInitDelegate } from '../../providers/init'; +import { CoreAppProvider } from '@providers/app'; +import { CoreFileProvider } from '@providers/file'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreInitDelegate } from '@providers/init'; /** * This module handles the emulation of Cordova plugins in browser and desktop. @@ -68,6 +71,7 @@ import { CoreInitDelegate } from '../../providers/init'; imports: [ ], providers: [ + Badge, // @todo: Mock CoreEmulatorHelperProvider, CoreEmulatorCaptureHelperProvider, { @@ -84,6 +88,7 @@ import { CoreInitDelegate } from '../../providers/init'; return appProvider.isMobile() ? new Clipboard() : new ClipboardMock(appProvider); } }, + Device, { provide: File, deps: [CoreAppProvider, CoreTextUtilsProvider], @@ -139,6 +144,7 @@ import { CoreInitDelegate } from '../../providers/init'; return platform.is('cordova') ? new Network() : new NetworkMock(); } }, + Push, // @todo: Mock SplashScreen, StatusBar, SQLite, diff --git a/src/core/emulator/pages/capture-media/capture-media.module.ts b/src/core/emulator/pages/capture-media/capture-media.module.ts index 133671a75..c849d91f4 100644 --- a/src/core/emulator/pages/capture-media/capture-media.module.ts +++ b/src/core/emulator/pages/capture-media/capture-media.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { CoreEmulatorCaptureMediaPage } from './capture-media'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreComponentsModule } from '@components/components.module'; @NgModule({ declarations: [ diff --git a/src/core/emulator/pages/capture-media/capture-media.ts b/src/core/emulator/pages/capture-media/capture-media.ts index 5ccdc7f7a..360183441 100644 --- a/src/core/emulator/pages/capture-media/capture-media.ts +++ b/src/core/emulator/pages/capture-media/capture-media.ts @@ -14,10 +14,10 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core'; import { IonicPage, ViewController, NavParams } from 'ionic-angular'; -import { CoreFileProvider } from '../../../../providers/file'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; -import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; -import { CoreTimeUtilsProvider } from '../../../../providers/utils/time'; +import { CoreFileProvider } from '@providers/file'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; /** * Page to capture media in browser or desktop. diff --git a/src/core/emulator/providers/capture-helper.ts b/src/core/emulator/providers/capture-helper.ts index 4d4ab87d9..cf66fbf92 100644 --- a/src/core/emulator/providers/capture-helper.ts +++ b/src/core/emulator/providers/capture-helper.ts @@ -14,8 +14,8 @@ import { Injectable } from '@angular/core'; import { ModalController, Modal } from 'ionic-angular'; -import { CoreMimetypeUtilsProvider } from '../../../providers/utils/mimetype'; -import { CoreUtilsProvider } from '../../../providers/utils/utils'; +import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; +import { CoreUtilsProvider } from '@providers/utils/utils'; /** * Helper service with some features to capture media (image, audio, video). diff --git a/src/core/emulator/providers/clipboard.ts b/src/core/emulator/providers/clipboard.ts index eb47fd6d6..40390a883 100644 --- a/src/core/emulator/providers/clipboard.ts +++ b/src/core/emulator/providers/clipboard.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { Clipboard } from '@ionic-native/clipboard'; -import { CoreAppProvider } from '../../../providers/app'; +import { CoreAppProvider } from '@providers/app'; /** * Emulates the Cordova Clipboard plugin in desktop apps and in browser. diff --git a/src/core/emulator/providers/file-transfer.ts b/src/core/emulator/providers/file-transfer.ts index 7c05fd8dd..7adff60e3 100644 --- a/src/core/emulator/providers/file-transfer.ts +++ b/src/core/emulator/providers/file-transfer.ts @@ -14,8 +14,8 @@ import { Injectable } from '@angular/core'; import { FileTransfer, FileTransferObject, FileUploadResult, FileTransferError } from '@ionic-native/file-transfer'; -import { CoreAppProvider } from '../../../providers/app'; -import { CoreFileProvider } from '../../../providers/file'; +import { CoreAppProvider } from '@providers/app'; +import { CoreFileProvider } from '@providers/file'; /** * Mock the File Transfer Error. diff --git a/src/core/emulator/providers/file.ts b/src/core/emulator/providers/file.ts index 8f756821c..cf6c77e8c 100644 --- a/src/core/emulator/providers/file.ts +++ b/src/core/emulator/providers/file.ts @@ -14,8 +14,8 @@ import { Injectable } from '@angular/core'; import { File, Entry, DirectoryEntry, FileEntry, FileError, IWriteOptions } from '@ionic-native/file'; -import { CoreAppProvider } from '../../../providers/app'; -import { CoreTextUtilsProvider } from '../../../providers/utils/text'; +import { CoreAppProvider } from '@providers/app'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreConfigConstants } from '../../../configconstants'; /** diff --git a/src/core/emulator/providers/globalization.ts b/src/core/emulator/providers/globalization.ts index 5ceeec8a9..6bcd6008c 100644 --- a/src/core/emulator/providers/globalization.ts +++ b/src/core/emulator/providers/globalization.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { Globalization } from '@ionic-native/globalization'; -import { CoreAppProvider } from '../../../providers/app'; +import { CoreAppProvider } from '@providers/app'; /** * Emulates the Cordova Globalization plugin in desktop apps and in browser. diff --git a/src/core/emulator/providers/helper.ts b/src/core/emulator/providers/helper.ts index cea59f24d..7314ded11 100644 --- a/src/core/emulator/providers/helper.ts +++ b/src/core/emulator/providers/helper.ts @@ -13,11 +13,11 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreFileProvider } from '../../../providers/file'; -import { CoreUtilsProvider } from '../../../providers/utils/utils'; +import { CoreFileProvider } from '@providers/file'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { File } from '@ionic-native/file'; import { LocalNotifications } from '@ionic-native/local-notifications'; -import { CoreInitDelegate, CoreInitHandler } from '../../../providers/init'; +import { CoreInitDelegate, CoreInitHandler } from '@providers/init'; import { FileTransferErrorMock } from './file-transfer'; import { CoreEmulatorCaptureHelperProvider } from './capture-helper'; diff --git a/src/core/emulator/providers/inappbrowser.ts b/src/core/emulator/providers/inappbrowser.ts index bcae07e73..21c18d3f2 100644 --- a/src/core/emulator/providers/inappbrowser.ts +++ b/src/core/emulator/providers/inappbrowser.ts @@ -14,9 +14,9 @@ import { Injectable } from '@angular/core'; import { InAppBrowser, InAppBrowserObject } from '@ionic-native/in-app-browser'; -import { CoreAppProvider } from '../../../providers/app'; -import { CoreFileProvider } from '../../../providers/file'; -import { CoreUrlUtilsProvider } from '../../../providers/utils/url'; +import { CoreAppProvider } from '@providers/app'; +import { CoreFileProvider } from '@providers/file'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { InAppBrowserObjectMock } from '../classes/inappbrowserobject'; /** diff --git a/src/core/emulator/providers/local-notifications.ts b/src/core/emulator/providers/local-notifications.ts index 9ea0c3318..6e19d6474 100644 --- a/src/core/emulator/providers/local-notifications.ts +++ b/src/core/emulator/providers/local-notifications.ts @@ -14,9 +14,9 @@ import { Injectable } from '@angular/core'; import { LocalNotifications, ILocalNotification } from '@ionic-native/local-notifications'; -import { CoreAppProvider } from '../../../providers/app'; -import { CoreUtilsProvider } from '../../../providers/utils/utils'; -import { SQLiteDB } from '../../../classes/sqlitedb'; +import { CoreAppProvider } from '@providers/app'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { SQLiteDB } from '@classes/sqlitedb'; import { CoreConstants } from '../../constants'; import { CoreConfigConstants } from '../../../configconstants'; import * as moment from 'moment'; diff --git a/src/core/emulator/providers/zip.ts b/src/core/emulator/providers/zip.ts index 2d3e27b77..9bc3d315a 100644 --- a/src/core/emulator/providers/zip.ts +++ b/src/core/emulator/providers/zip.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { Zip } from '@ionic-native/zip'; import { JSZip } from 'jszip'; import { File } from '@ionic-native/file'; -import { CoreMimetypeUtilsProvider } from '../../../providers/utils/mimetype'; +import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; /** * Emulates the Cordova Zip plugin in desktop apps and in browser. diff --git a/src/core/fileuploader/providers/album-handler.ts b/src/core/fileuploader/providers/album-handler.ts index b1221ef34..729539db1 100644 --- a/src/core/fileuploader/providers/album-handler.ts +++ b/src/core/fileuploader/providers/album-handler.ts @@ -13,8 +13,8 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreAppProvider } from '../../../providers/app'; -import { CoreUtilsProvider } from '../../../providers/utils/utils'; +import { CoreAppProvider } from '@providers/app'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFileUploaderHandler, CoreFileUploaderHandlerData } from './delegate'; import { CoreFileUploaderHelperProvider } from './helper'; /** diff --git a/src/core/fileuploader/providers/audio-handler.ts b/src/core/fileuploader/providers/audio-handler.ts index a3d8a2cd0..c6c73b9c2 100644 --- a/src/core/fileuploader/providers/audio-handler.ts +++ b/src/core/fileuploader/providers/audio-handler.ts @@ -14,8 +14,8 @@ import { Injectable } from '@angular/core'; import { Platform } from 'ionic-angular'; -import { CoreAppProvider } from '../../../providers/app'; -import { CoreUtilsProvider } from '../../../providers/utils/utils'; +import { CoreAppProvider } from '@providers/app'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFileUploaderHandler, CoreFileUploaderHandlerData } from './delegate'; import { CoreFileUploaderHelperProvider } from './helper'; /** diff --git a/src/core/fileuploader/providers/camera-handler.ts b/src/core/fileuploader/providers/camera-handler.ts index 9e772ecea..c4a9db2d3 100644 --- a/src/core/fileuploader/providers/camera-handler.ts +++ b/src/core/fileuploader/providers/camera-handler.ts @@ -13,8 +13,8 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreAppProvider } from '../../../providers/app'; -import { CoreUtilsProvider } from '../../../providers/utils/utils'; +import { CoreAppProvider } from '@providers/app'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFileUploaderHandler, CoreFileUploaderHandlerData } from './delegate'; import { CoreFileUploaderHelperProvider } from './helper'; /** diff --git a/src/core/fileuploader/providers/delegate.ts b/src/core/fileuploader/providers/delegate.ts index 5187e3c9e..e0b149c81 100644 --- a/src/core/fileuploader/providers/delegate.ts +++ b/src/core/fileuploader/providers/delegate.ts @@ -13,10 +13,10 @@ // 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 { CoreDelegate, CoreDelegateHandler } from '../../../classes/delegate'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; /** * Interface that all handlers must implement. diff --git a/src/core/fileuploader/providers/file-handler.ts b/src/core/fileuploader/providers/file-handler.ts index e045682f2..4479880f0 100644 --- a/src/core/fileuploader/providers/file-handler.ts +++ b/src/core/fileuploader/providers/file-handler.ts @@ -14,9 +14,9 @@ import { Injectable } from '@angular/core'; import { Platform } from 'ionic-angular'; -import { CoreAppProvider } from '../../../providers/app'; -import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; -import { CoreTimeUtilsProvider } from '../../../providers/utils/time'; +import { CoreAppProvider } from '@providers/app'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreFileUploaderHandler, CoreFileUploaderHandlerData } from './delegate'; import { CoreFileUploaderHelperProvider } from './helper'; import { CoreFileUploaderProvider } from './fileuploader'; diff --git a/src/core/fileuploader/providers/fileuploader.ts b/src/core/fileuploader/providers/fileuploader.ts index abc48fbc5..4934ead60 100644 --- a/src/core/fileuploader/providers/fileuploader.ts +++ b/src/core/fileuploader/providers/fileuploader.ts @@ -16,15 +16,15 @@ import { Injectable } from '@angular/core'; import { Platform } from 'ionic-angular'; import { MediaFile } from '@ionic-native/media-capture'; import { TranslateService } from '@ngx-translate/core'; -import { CoreFileProvider } from '../../../providers/file'; -import { CoreFilepoolProvider } from '../../../providers/filepool'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreMimetypeUtilsProvider } from '../../../providers/utils/mimetype'; -import { CoreTextUtilsProvider } from '../../../providers/utils/text'; -import { CoreTimeUtilsProvider } from '../../../providers/utils/time'; -import { CoreUtilsProvider } from '../../../providers/utils/utils'; -import { CoreWSFileUploadOptions } from '../../../providers/ws'; +import { CoreFileProvider } from '@providers/file'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreWSFileUploadOptions } from '@providers/ws'; /** * File upload options. diff --git a/src/core/fileuploader/providers/helper.ts b/src/core/fileuploader/providers/helper.ts index e955869a3..3518c3b54 100644 --- a/src/core/fileuploader/providers/helper.ts +++ b/src/core/fileuploader/providers/helper.ts @@ -17,12 +17,12 @@ import { ActionSheetController, ActionSheet, Platform } from 'ionic-angular'; import { MediaCapture, MediaFile } from '@ionic-native/media-capture'; import { Camera, CameraOptions } from '@ionic-native/camera'; import { TranslateService } from '@ngx-translate/core'; -import { CoreAppProvider } from '../../../providers/app'; -import { CoreFileProvider } from '../../../providers/file'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; -import { CoreTextUtilsProvider } from '../../../providers/utils/text'; -import { CoreUtilsProvider, PromiseDefer } from '../../../providers/utils/utils'; +import { CoreAppProvider } from '@providers/app'; +import { CoreFileProvider } from '@providers/file'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider, PromiseDefer } from '@providers/utils/utils'; import { CoreFileUploaderProvider, CoreFileUploaderOptions } from './fileuploader'; import { CoreFileUploaderDelegate } from './delegate'; diff --git a/src/core/fileuploader/providers/video-handler.ts b/src/core/fileuploader/providers/video-handler.ts index 8d66019e0..22c70ecac 100644 --- a/src/core/fileuploader/providers/video-handler.ts +++ b/src/core/fileuploader/providers/video-handler.ts @@ -14,8 +14,8 @@ import { Injectable } from '@angular/core'; import { Platform } from 'ionic-angular'; -import { CoreAppProvider } from '../../../providers/app'; -import { CoreUtilsProvider } from '../../../providers/utils/utils'; +import { CoreAppProvider } from '@providers/app'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFileUploaderHandler, CoreFileUploaderHandlerData } from './delegate'; import { CoreFileUploaderHelperProvider } from './helper'; /** diff --git a/src/core/grades/components/components.module.ts b/src/core/grades/components/components.module.ts index 5e50f4d7a..872bb100f 100644 --- a/src/core/grades/components/components.module.ts +++ b/src/core/grades/components/components.module.ts @@ -17,9 +17,9 @@ import { CommonModule } from '@angular/common'; import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreGradesCourseComponent } from './course/course'; -import { CoreComponentsModule } from '../../../components/components.module'; -import { CoreDirectivesModule } from '../../../directives/directives.module'; -import { CorePipesModule } from '../../../pipes/pipes.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; +import { CorePipesModule } from '@pipes'; @NgModule({ declarations: [ diff --git a/src/core/grades/components/course/course.ts b/src/core/grades/components/course/course.ts index bdbe66489..181080f4d 100644 --- a/src/core/grades/components/course/course.ts +++ b/src/core/grades/components/course/course.ts @@ -15,11 +15,11 @@ import { Component, ViewChild, Input, Optional } from '@angular/core'; import { Content, NavParams, NavController } from 'ionic-angular'; import { CoreGradesProvider } from '../../providers/grades'; -import { CoreSitesProvider } from '../../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreGradesHelperProvider } from '../../providers/helper'; -import { CoreSplitViewComponent } from '../../../../components/split-view/split-view'; -import { CoreAppProvider } from '../../../../providers/app'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreAppProvider } from '@providers/app'; /** * Component that displays a course grades. diff --git a/src/core/grades/grades.module.ts b/src/core/grades/grades.module.ts index 44bd085fb..d090f2e00 100644 --- a/src/core/grades/grades.module.ts +++ b/src/core/grades/grades.module.ts @@ -25,8 +25,8 @@ import { CoreGradesOverviewLinkHandler } from './providers/overview-link-handler import { CoreContentLinksDelegate } from '../contentlinks/providers/delegate'; import { CoreGradesUserHandler } from './providers/user-handler'; import { CoreUserDelegate } from '../user/providers/user-delegate'; -import { CoreEventsProvider } from '../../providers/events'; -import { CoreSitesProvider } from '../../providers/sites'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; import { CoreUserProvider } from '../user/providers/user'; @NgModule({ diff --git a/src/core/grades/pages/course/course.ts b/src/core/grades/pages/course/course.ts index ce40b75e5..af07df6a3 100644 --- a/src/core/grades/pages/course/course.ts +++ b/src/core/grades/pages/course/course.ts @@ -14,7 +14,7 @@ import { Component } from '@angular/core'; import { IonicPage, NavParams } from 'ionic-angular'; -import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreSitesProvider } from '@providers/sites'; /** * Page that displays a course grades. diff --git a/src/core/grades/pages/courses/courses.module.ts b/src/core/grades/pages/courses/courses.module.ts index b4c8b834d..05bb74eab 100644 --- a/src/core/grades/pages/courses/courses.module.ts +++ b/src/core/grades/pages/courses/courses.module.ts @@ -16,8 +16,8 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreGradesCoursesPage } from './courses'; -import { CoreComponentsModule } from '../../../../components/components.module'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/core/grades/pages/courses/courses.ts b/src/core/grades/pages/courses/courses.ts index 4610fdce7..14a7e83dd 100644 --- a/src/core/grades/pages/courses/courses.ts +++ b/src/core/grades/pages/courses/courses.ts @@ -15,8 +15,8 @@ import { Component, ViewChild } from '@angular/core'; import { IonicPage, Content } from 'ionic-angular'; import { CoreGradesProvider } from '../../providers/grades'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; -import { CoreSplitViewComponent } from '../../../../components/split-view/split-view'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreGradesHelperProvider } from '../../providers/helper'; /** diff --git a/src/core/grades/pages/coursesplit/coursesplit.module.ts b/src/core/grades/pages/coursesplit/coursesplit.module.ts index fb9bab05a..18775db1d 100644 --- a/src/core/grades/pages/coursesplit/coursesplit.module.ts +++ b/src/core/grades/pages/coursesplit/coursesplit.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreGradesCourseSplitPage } from './coursesplit'; -import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreComponentsModule } from '@components/components.module'; import { CoreGradesComponentsModule } from '../../components/components.module'; @NgModule({ diff --git a/src/core/grades/pages/coursesplit/coursesplit.ts b/src/core/grades/pages/coursesplit/coursesplit.ts index f759b72ca..beca318b2 100644 --- a/src/core/grades/pages/coursesplit/coursesplit.ts +++ b/src/core/grades/pages/coursesplit/coursesplit.ts @@ -14,7 +14,7 @@ import { Component } from '@angular/core'; import { IonicPage, NavParams } from 'ionic-angular'; -import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreSitesProvider } from '@providers/sites'; /** * Page that displays a course grades. diff --git a/src/core/grades/pages/grade/grade.module.ts b/src/core/grades/pages/grade/grade.module.ts index 5b591fddd..159a3c28e 100644 --- a/src/core/grades/pages/grade/grade.module.ts +++ b/src/core/grades/pages/grade/grade.module.ts @@ -16,8 +16,8 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreGradesGradePage } from './grade'; -import { CoreComponentsModule } from '../../../../components/components.module'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/core/grades/pages/grade/grade.ts b/src/core/grades/pages/grade/grade.ts index 181b1d6f1..fc94c0497 100644 --- a/src/core/grades/pages/grade/grade.ts +++ b/src/core/grades/pages/grade/grade.ts @@ -15,9 +15,9 @@ import { Component, ViewChild } from '@angular/core'; import { IonicPage, Content, NavParams } from 'ionic-angular'; import { CoreGradesProvider } from '../../providers/grades'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreGradesHelperProvider } from '../../providers/helper'; -import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreSitesProvider } from '@providers/sites'; /** * Page that displays activity grade. diff --git a/src/core/grades/providers/course-option-handler.ts b/src/core/grades/providers/course-option-handler.ts index d2cd4028e..54f9396e7 100644 --- a/src/core/grades/providers/course-option-handler.ts +++ b/src/core/grades/providers/course-option-handler.ts @@ -80,7 +80,7 @@ export class CoreGradesCourseOptionHandler implements CoreCourseOptionsHandler { /** * Returns the data needed to render the handler. * - * @return {CoreMainMenuHandlerData} Data needed to render the handler. + * @return {CoreCourseOptionsHandlerData} Data needed to render the handler. */ getDisplayData(): CoreCourseOptionsHandlerData { return { diff --git a/src/core/grades/providers/grades.ts b/src/core/grades/providers/grades.ts index 21cf6afe3..15f21e1b4 100644 --- a/src/core/grades/providers/grades.ts +++ b/src/core/grades/providers/grades.ts @@ -13,9 +13,9 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSite } from '../../../classes/site'; -import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSite } from '@classes/site'; +import { CoreSitesProvider } from '@providers/sites'; import { CoreCoursesProvider } from '../../courses/providers/courses'; /** diff --git a/src/core/grades/providers/helper.ts b/src/core/grades/providers/helper.ts index 8c7a42dfc..ceffae450 100644 --- a/src/core/grades/providers/helper.ts +++ b/src/core/grades/providers/helper.ts @@ -13,15 +13,15 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; import { TranslateService } from '@ngx-translate/core'; import { CoreCoursesProvider } from '../../courses/providers/courses'; import { CoreCourseProvider } from '../../course/providers/course'; import { CoreGradesProvider } from './grades'; -import { CoreTextUtilsProvider } from '../../../providers/utils/text'; -import { CoreUrlUtilsProvider } from '../../../providers/utils/url'; -import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; /** * Service that provides some features regarding grades information. diff --git a/src/core/grades/providers/mainmenu-handler.ts b/src/core/grades/providers/mainmenu-handler.ts index ff9ee2ad7..59c2b016e 100644 --- a/src/core/grades/providers/mainmenu-handler.ts +++ b/src/core/grades/providers/mainmenu-handler.ts @@ -22,7 +22,7 @@ import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../mainmenu/pro @Injectable() export class CoreGradesMainMenuHandler implements CoreMainMenuHandler { name = 'CoreGrades'; - priority = 950; + priority = 600; constructor(private gradesProvider: CoreGradesProvider) { } diff --git a/src/core/grades/providers/user-handler.ts b/src/core/grades/providers/user-handler.ts index 593b7cdab..ba95dc857 100644 --- a/src/core/grades/providers/user-handler.ts +++ b/src/core/grades/providers/user-handler.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '../../user/providers/user-delegate'; -import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreSitesProvider } from '@providers/sites'; import { CoreContentLinksHelperProvider } from '../../contentlinks/providers/helper'; import { CoreGradesProvider } from './grades'; diff --git a/src/core/login/pages/credentials/credentials.module.ts b/src/core/login/pages/credentials/credentials.module.ts index 88e6e955e..a5b767d10 100644 --- a/src/core/login/pages/credentials/credentials.module.ts +++ b/src/core/login/pages/credentials/credentials.module.ts @@ -16,8 +16,8 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { CoreLoginCredentialsPage } from './credentials'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreComponentsModule } from '../../../../components/components.module'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/core/login/pages/credentials/credentials.ts b/src/core/login/pages/credentials/credentials.ts index 790ea8df5..75ffd2b0b 100644 --- a/src/core/login/pages/credentials/credentials.ts +++ b/src/core/login/pages/credentials/credentials.ts @@ -15,11 +15,11 @@ import { Component } from '@angular/core'; import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -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 { 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 { CoreContentLinksDelegate } from '../../../contentlinks/providers/delegate'; import { CoreContentLinksHelperProvider } from '../../../contentlinks/providers/helper'; diff --git a/src/core/login/pages/email-signup/email-signup.module.ts b/src/core/login/pages/email-signup/email-signup.module.ts index 422f4271f..cf128a353 100644 --- a/src/core/login/pages/email-signup/email-signup.module.ts +++ b/src/core/login/pages/email-signup/email-signup.module.ts @@ -16,8 +16,8 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { CoreLoginEmailSignupPage } from './email-signup'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreComponentsModule } from '../../../../components/components.module'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; import { CoreUserComponentsModule } from '../../../user/components/components.module'; @NgModule({ diff --git a/src/core/login/pages/email-signup/email-signup.ts b/src/core/login/pages/email-signup/email-signup.ts index cbe3349b5..606178f13 100644 --- a/src/core/login/pages/email-signup/email-signup.ts +++ b/src/core/login/pages/email-signup/email-signup.ts @@ -15,11 +15,11 @@ import { Component, ViewChild } from '@angular/core'; import { IonicPage, NavController, NavParams, Content } 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 { CoreUtilsProvider } from '../../../../providers/utils/utils'; -import { CoreWSProvider } from '../../../../providers/ws'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreWSProvider } from '@providers/ws'; import { CoreLoginHelperProvider } from '../../providers/helper'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { CoreUserProfileFieldDelegate } from '../../../user/providers/user-profile-field-delegate'; diff --git a/src/core/login/pages/forgotten-password/forgotten-password.ts b/src/core/login/pages/forgotten-password/forgotten-password.ts index 1c37f23ab..3aec2d353 100644 --- a/src/core/login/pages/forgotten-password/forgotten-password.ts +++ b/src/core/login/pages/forgotten-password/forgotten-password.ts @@ -15,7 +15,7 @@ import { Component } from '@angular/core'; import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreLoginHelperProvider } from '../../providers/helper'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; diff --git a/src/core/login/pages/init/init.ts b/src/core/login/pages/init/init.ts index c5ccb35aa..2770bcf4e 100644 --- a/src/core/login/pages/init/init.ts +++ b/src/core/login/pages/init/init.ts @@ -14,9 +14,9 @@ import { Component } from '@angular/core'; import { IonicPage, NavController } from 'ionic-angular'; -import { CoreAppProvider } from '../../../../providers/app'; -import { CoreInitDelegate } from '../../../../providers/init'; -import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreAppProvider } from '@providers/app'; +import { CoreInitDelegate } from '@providers/init'; +import { CoreSitesProvider } from '@providers/sites'; import { CoreConstants } from '../../../constants'; import { CoreLoginHelperProvider } from '../../providers/helper'; @@ -77,10 +77,12 @@ export class CoreLoginInitPage { this.loginHelper.goToSiteInitialPage(); } } else { - this.sitesProvider.hasSites().then(() => { - this.navCtrl.setRoot('CoreLoginSitesPage'); - }, () => { - this.loginHelper.goToAddSite(true); + this.sitesProvider.hasSites().then((hasSites) => { + if (hasSites) { + this.navCtrl.setRoot('CoreLoginSitesPage'); + } else { + this.loginHelper.goToAddSite(true); + } }); } } diff --git a/src/core/login/pages/reconnect/reconnect.html b/src/core/login/pages/reconnect/reconnect.html index d3c6b5903..ee94ea5e6 100644 --- a/src/core/login/pages/reconnect/reconnect.html +++ b/src/core/login/pages/reconnect/reconnect.html @@ -8,7 +8,7 @@
- {{ 'core.pictureof' | translate:{$a: site.fullname} }} + {{ 'core.pictureof' | translate:{$a: site.fullname} }} diff --git a/src/core/login/pages/reconnect/reconnect.module.ts b/src/core/login/pages/reconnect/reconnect.module.ts index 10260486f..40f59485f 100644 --- a/src/core/login/pages/reconnect/reconnect.module.ts +++ b/src/core/login/pages/reconnect/reconnect.module.ts @@ -16,8 +16,8 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreLoginReconnectPage } from './reconnect'; -import { CoreComponentsModule } from '../../../../components/components.module'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/core/login/pages/reconnect/reconnect.ts b/src/core/login/pages/reconnect/reconnect.ts index cbbabea19..1dc684011 100644 --- a/src/core/login/pages/reconnect/reconnect.ts +++ b/src/core/login/pages/reconnect/reconnect.ts @@ -14,9 +14,9 @@ import { Component } from '@angular/core'; import { IonicPage, NavController, NavParams } from 'ionic-angular'; -import { CoreAppProvider } from '../../../../providers/app'; -import { CoreSitesProvider } from '../../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreAppProvider } from '@providers/app'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreLoginHelperProvider } from '../../providers/helper'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; diff --git a/src/core/login/pages/site-error/site-error.module.ts b/src/core/login/pages/site-error/site-error.module.ts index 30b2036d5..60284f65f 100644 --- a/src/core/login/pages/site-error/site-error.module.ts +++ b/src/core/login/pages/site-error/site-error.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { CoreLoginSiteErrorPage } from './site-error'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/core/login/pages/site-help/site-help.module.ts b/src/core/login/pages/site-help/site-help.module.ts index 942199b3d..c9f01d134 100644 --- a/src/core/login/pages/site-help/site-help.module.ts +++ b/src/core/login/pages/site-help/site-help.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { CoreLoginSiteHelpPage } from './site-help'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/core/login/pages/site-policy/site-policy.module.ts b/src/core/login/pages/site-policy/site-policy.module.ts index c029e1c64..65475306b 100644 --- a/src/core/login/pages/site-policy/site-policy.module.ts +++ b/src/core/login/pages/site-policy/site-policy.module.ts @@ -16,8 +16,8 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { CoreLoginSitePolicyPage } from './site-policy'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreComponentsModule } from '../../../../components/components.module'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/core/login/pages/site-policy/site-policy.ts b/src/core/login/pages/site-policy/site-policy.ts index c9fc76eed..c63026f8c 100644 --- a/src/core/login/pages/site-policy/site-policy.ts +++ b/src/core/login/pages/site-policy/site-policy.ts @@ -14,11 +14,11 @@ import { Component } from '@angular/core'; import { IonicPage, NavController, NavParams } from 'ionic-angular'; -import { CoreSitesProvider } from '../../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; -import { CoreMimetypeUtilsProvider } from '../../../../providers/utils/mimetype'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreLoginHelperProvider } from '../../providers/helper'; -import { CoreSite } from '../../../../classes/site'; +import { CoreSite } from '@classes/site'; /** * Page to accept a site policy. diff --git a/src/core/login/pages/site/site.module.ts b/src/core/login/pages/site/site.module.ts index b446ba9fb..a188fdfc9 100644 --- a/src/core/login/pages/site/site.module.ts +++ b/src/core/login/pages/site/site.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { CoreLoginSitePage } from './site'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/core/login/pages/site/site.ts b/src/core/login/pages/site/site.ts index fd7799502..77bb3eb2c 100644 --- a/src/core/login/pages/site/site.ts +++ b/src/core/login/pages/site/site.ts @@ -14,9 +14,9 @@ import { Component } from '@angular/core'; import { IonicPage, NavController, ModalController } from 'ionic-angular'; -import { CoreAppProvider } from '../../../../providers/app'; -import { CoreSitesProvider } from '../../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreAppProvider } from '@providers/app'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreConfigConstants } from '../../../../configconstants'; import { CoreLoginHelperProvider } from '../../providers/helper'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; diff --git a/src/core/login/pages/sites/sites.html b/src/core/login/pages/sites/sites.html index f4fd0baf9..4e57f2aa5 100644 --- a/src/core/login/pages/sites/sites.html +++ b/src/core/login/pages/sites/sites.html @@ -13,7 +13,7 @@ - {{ 'core.pictureof' | translate:{$a: site.fullname} }} + {{ 'core.pictureof' | translate:{$a: site.fullname} }}

{{site.fullName}}

diff --git a/src/core/login/pages/sites/sites.module.ts b/src/core/login/pages/sites/sites.module.ts index c643dbe4e..5bf8716fd 100644 --- a/src/core/login/pages/sites/sites.module.ts +++ b/src/core/login/pages/sites/sites.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreLoginSitesPage } from './sites'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/core/login/pages/sites/sites.ts b/src/core/login/pages/sites/sites.ts index bbc36bb8c..77dfb0d73 100644 --- a/src/core/login/pages/sites/sites.ts +++ b/src/core/login/pages/sites/sites.ts @@ -15,10 +15,11 @@ import { Component } from '@angular/core'; import { IonicPage } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreLoggerProvider } from '../../../../providers/logger'; -import { CoreSitesProvider, CoreSiteBasicInfo } from '../../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; -import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider, CoreSiteBasicInfo } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonPushNotificationsProvider } from '@addon/pushnotifications/providers/pushnotifications'; import { CoreLoginHelperProvider } from '../../providers/helper'; /** @@ -35,8 +36,8 @@ export class CoreLoginSitesPage { protected logger; constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, - private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider, - private translate: TranslateService, logger: CoreLoggerProvider) { + private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider, logger: CoreLoggerProvider, + private translate: TranslateService, private pushNotificationsProvider: AddonPushNotificationsProvider) { this.logger = logger.getInstance('CoreLoginSitesPage'); } @@ -48,8 +49,10 @@ export class CoreLoginSitesPage { // Remove protocol from the url to show more url text. sites = sites.map((site) => { site.siteUrl = site.siteUrl.replace(/^https?:\/\//, ''); - site.badge = 10; - // @todo: Implement it once push notifications addon is implemented: $mmaPushNotifications.getSiteCounter(site.id) + site.badge = 0; + this.pushNotificationsProvider.getSiteCounter(site.id).then((counter) => { + site.badge = counter; + }); return site; }); @@ -104,8 +107,10 @@ export class CoreLoginSitesPage { this.showDelete = false; // If there are no sites left, go to add site. - this.sitesProvider.hasNoSites().then(() => { - this.loginHelper.goToAddSite(true); + this.sitesProvider.hasSites().then((hasSites) => { + if (!hasSites) { + this.loginHelper.goToAddSite(true); + } }); }).catch((error) => { this.logger.error('Error deleting site ' + site.id, error); diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index 6068586ad..d75c7f3e5 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -15,17 +15,17 @@ import { Injectable } from '@angular/core'; import { Platform } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreAppProvider } from '../../../providers/app'; -import { CoreConfigProvider } from '../../../providers/config'; -import { CoreEventsProvider } from '../../../providers/events'; -import { CoreInitDelegate } from '../../../providers/init'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreWSProvider } from '../../../providers/ws'; -import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; -import { CoreTextUtilsProvider } from '../../../providers/utils/text'; -import { CoreUrlUtilsProvider } from '../../../providers/utils/url'; -import { CoreUtilsProvider } from '../../../providers/utils/utils'; +import { CoreAppProvider } from '@providers/app'; +import { CoreConfigProvider } from '@providers/config'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreInitDelegate } from '@providers/init'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreWSProvider } from '@providers/ws'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreConfigConstants } from '../../../configconstants'; import { CoreConstants } from '../../constants'; import { Md5 } from 'ts-md5/dist/md5'; @@ -901,7 +901,7 @@ export class CoreLoginHelperProvider { return; } - if (!this.sitesProvider.getCurrentSite().wsAvailable('core_user_agree_site_policy')) { + if (!this.sitesProvider.wsAvailableInCurrentSite('core_user_agree_site_policy')) { // WS not available, stop. return; } diff --git a/src/core/mainmenu/pages/menu/menu.html b/src/core/mainmenu/pages/menu/menu.html index be5610957..f053d132d 100644 --- a/src/core/mainmenu/pages/menu/menu.html +++ b/src/core/mainmenu/pages/menu/menu.html @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/src/core/mainmenu/pages/menu/menu.ts b/src/core/mainmenu/pages/menu/menu.ts index 016dfe398..acd3a99eb 100644 --- a/src/core/mainmenu/pages/menu/menu.ts +++ b/src/core/mainmenu/pages/menu/menu.ts @@ -14,8 +14,8 @@ import { Component, OnDestroy, ViewChild } from '@angular/core'; import { IonicPage, NavController, NavParams, Tabs } from 'ionic-angular'; -import { CoreEventsProvider } from '../../../../providers/events'; -import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; import { CoreMainMenuProvider } from '../../providers/mainmenu'; import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../providers/delegate'; @@ -64,9 +64,10 @@ export class CoreMainMenuPage implements OnDestroy { }; protected moreTabAdded = false; protected redirectPageLoaded = false; + protected updateBadgeObserver; constructor(private menuDelegate: CoreMainMenuDelegate, private sitesProvider: CoreSitesProvider, navParams: NavParams, - private navCtrl: NavController, eventsProvider: CoreEventsProvider) { + private navCtrl: NavController, private eventsProvider: CoreEventsProvider) { this.redirectPage = navParams.get('redirectPage'); this.redirectParams = navParams.get('redirectParams'); } @@ -84,6 +85,15 @@ export class CoreMainMenuPage implements OnDestroy { const site = this.sitesProvider.getCurrentSite(), displaySiteHome = site.getInfo() && site.getInfo().userhomepage === 0; + this.updateBadgeObserver = this.eventsProvider.on(CoreMainMenuDelegate.UPDATE_BADGE_EVENT, (data) => { + const tab = this.tabs.find((tab) => { + return tab.showBadge && tab['name'] == data.name; + }); + if (tab) { + tab.badge = data.badge; + } + }, site.getId()); + this.subscription = this.menuDelegate.getHandlers().subscribe((handlers) => { handlers = handlers.slice(0, CoreMainMenuProvider.NUM_MAIN_HANDLERS); // Get main handlers. @@ -128,5 +138,6 @@ export class CoreMainMenuPage implements OnDestroy { */ ngOnDestroy(): void { this.subscription && this.subscription.unsubscribe(); + this.updateBadgeObserver && this.updateBadgeObserver.off(); } } diff --git a/src/core/mainmenu/pages/more/more.html b/src/core/mainmenu/pages/more/more.html index 089e4ad3c..63c01f41f 100644 --- a/src/core/mainmenu/pages/more/more.html +++ b/src/core/mainmenu/pages/more/more.html @@ -15,12 +15,11 @@ - +

{{ handler.title | translate}}

- - + {{badge}} +
diff --git a/src/core/mainmenu/pages/more/more.module.ts b/src/core/mainmenu/pages/more/more.module.ts index f755c35c6..0f2e6b72c 100644 --- a/src/core/mainmenu/pages/more/more.module.ts +++ b/src/core/mainmenu/pages/more/more.module.ts @@ -16,8 +16,8 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreMainMenuMorePage } from './more'; -import { CoreComponentsModule } from '../../../../components/components.module'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/core/mainmenu/pages/more/more.ts b/src/core/mainmenu/pages/more/more.ts index 084b061d9..0c396bfd9 100644 --- a/src/core/mainmenu/pages/more/more.ts +++ b/src/core/mainmenu/pages/more/more.ts @@ -14,8 +14,8 @@ import { Component, OnDestroy } from '@angular/core'; import { IonicPage, NavController } from 'ionic-angular'; -import { CoreEventsProvider } from '../../../../providers/events'; -import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../providers/delegate'; import { CoreMainMenuProvider, CoreMainMenuCustomItem } from '../../providers/mainmenu'; @@ -40,9 +40,11 @@ export class CoreMainMenuMorePage implements OnDestroy { protected subscription; protected langObserver; protected updateSiteObserver; + protected updateBadgeObserver; constructor(private menuDelegate: CoreMainMenuDelegate, private sitesProvider: CoreSitesProvider, - private navCtrl: NavController, private mainMenuProvider: CoreMainMenuProvider, eventsProvider: CoreEventsProvider) { + private navCtrl: NavController, private mainMenuProvider: CoreMainMenuProvider, + private eventsProvider: CoreEventsProvider) { this.langObserver = eventsProvider.on(CoreEventsProvider.LANGUAGE_CHANGED, this.loadSiteInfo.bind(this)); this.updateSiteObserver = eventsProvider.on(CoreEventsProvider.SITE_UPDATED, this.loadSiteInfo.bind(this), @@ -59,6 +61,15 @@ export class CoreMainMenuMorePage implements OnDestroy { this.handlers = handlers.slice(CoreMainMenuProvider.NUM_MAIN_HANDLERS); // Remove the main handlers. this.handlersLoaded = this.menuDelegate.areHandlersLoaded(); }); + + this.updateBadgeObserver = this.eventsProvider.on(CoreMainMenuDelegate.UPDATE_BADGE_EVENT, (data) => { + const handler = this.handlers.find((handler) => { + return handler.showBadge && handler['name'] == data.name; + }); + if (handler) { + handler.badge = data.badge; + } + }, this.sitesProvider.getCurrentSiteId()); } /** @@ -68,6 +79,7 @@ export class CoreMainMenuMorePage implements OnDestroy { if (this.subscription) { this.subscription.unsubscribe(); } + this.updateBadgeObserver && this.updateBadgeObserver.off(); } /** @@ -97,7 +109,7 @@ export class CoreMainMenuMorePage implements OnDestroy { * @param {CoreMainMenuHandlerData} handler Handler to open. */ openHandler(handler: CoreMainMenuHandlerData): void { - // @todo. + this.navCtrl.push(handler.page); } /** diff --git a/src/core/mainmenu/providers/delegate.ts b/src/core/mainmenu/providers/delegate.ts index 191cafbdf..5d8b64865 100644 --- a/src/core/mainmenu/providers/delegate.ts +++ b/src/core/mainmenu/providers/delegate.ts @@ -13,10 +13,10 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreEventsProvider } from '../../../providers/events'; -import { CoreDelegate, CoreDelegateHandler } from '../../../classes/delegate'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; import { Subject, BehaviorSubject } from 'rxjs'; /** @@ -32,7 +32,6 @@ export interface CoreMainMenuHandler extends CoreDelegateHandler { /** * Returns the data needed to render the handler. * - * @param {number} courseId The course ID. * @return {CoreMainMenuHandlerData} Data. */ getDisplayData(): CoreMainMenuHandlerData; @@ -65,13 +64,30 @@ export interface CoreMainMenuHandlerData { * @type {string} */ class?: string; + + /** + * If the handler has badge to show or not. + * @type {boolean} + */ + showBadge?: boolean; + + /** + * Text to display on the badge. Only used if showBadge is true. + * @type {string} + */ + badge?: string; + + /** + * If true, the badge number is being loaded. Only used if showBadge is true. + * @type {boolean} + */ + loading?: boolean; } /** * Data returned by the delegate for each handler. */ export interface CoreMainMenuHandlerToDisplay extends CoreMainMenuHandlerData { - /** * Name of the handler. * @type {string} @@ -91,6 +107,8 @@ export class CoreMainMenuDelegate extends CoreDelegate { protected siteHandlers: Subject = new BehaviorSubject([]); protected featurePrefix = '$mmSideMenuDelegate_'; + static UPDATE_BADGE_EVENT = 'update_main_menu_badge'; + constructor(protected loggerProvider: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider, protected eventsProvider: CoreEventsProvider) { super('CoreMainMenuDelegate', loggerProvider, sitesProvider, eventsProvider); @@ -135,6 +153,7 @@ export class CoreMainMenuDelegate extends CoreDelegate { data = handler.getDisplayData(); handlersData.push({ + name: name, data: data, priority: handler.priority }); @@ -147,6 +166,8 @@ export class CoreMainMenuDelegate extends CoreDelegate { // Return only the display data. const displayData = handlersData.map((item) => { + item.data.name = item.name; + return item.data; }); diff --git a/src/core/mainmenu/providers/mainmenu.ts b/src/core/mainmenu/providers/mainmenu.ts index 679445442..abcec6f65 100644 --- a/src/core/mainmenu/providers/mainmenu.ts +++ b/src/core/mainmenu/providers/mainmenu.ts @@ -13,8 +13,8 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreLangProvider } from '../../../providers/lang'; -import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreLangProvider } from '@providers/lang'; +import { CoreSitesProvider } from '@providers/sites'; import { CoreConfigConstants } from '../../../configconstants'; /** diff --git a/src/core/settings/lang/en.json b/src/core/settings/lang/en.json new file mode 100644 index 000000000..6e7ad59e0 --- /dev/null +++ b/src/core/settings/lang/en.json @@ -0,0 +1,10 @@ +{ + "about": "About", + "general": "General", + "loggedin": "Online", + "loggedoff": "Offline", + "settings": "Settings", + "sites": "Sites", + "spaceusage": "Space usage", + "synchronization": "Synchronisation" +} \ No newline at end of file diff --git a/src/core/settings/pages/list/list.html b/src/core/settings/pages/list/list.html new file mode 100644 index 000000000..b26591fe8 --- /dev/null +++ b/src/core/settings/pages/list/list.html @@ -0,0 +1,37 @@ + + + {{ 'core.settings.settings' | translate}} + + + + + + + +

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

+
+ + +

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

+
+ + +

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

+
+ + +

{{ 'core.sharedfiles.sharedfiles' | translate }}

+
+ + + +

{{ handler.title | translate}}

+
+ + + +

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

+
+
+
+
diff --git a/src/core/settings/pages/list/list.module.ts b/src/core/settings/pages/list/list.module.ts new file mode 100644 index 000000000..5c2fcaf3a --- /dev/null +++ b/src/core/settings/pages/list/list.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 { CoreSettingsListPage } from './list'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; + +@NgModule({ + declarations: [ + CoreSettingsListPage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(CoreSettingsListPage), + TranslateModule.forChild() + ], +}) +export class CoreSettingsListPageModule {} diff --git a/src/core/settings/pages/list/list.ts b/src/core/settings/pages/list/list.ts new file mode 100644 index 000000000..28803a351 --- /dev/null +++ b/src/core/settings/pages/list/list.ts @@ -0,0 +1,66 @@ +// (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, NavController, NavParams, Platform } from 'ionic-angular'; +import { CoreSettingsDelegate, CoreSettingsHandlerData } from '../../providers/delegate'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; + +/** + * Page that displays the list of settings pages. + */ +@IonicPage({segment: 'core-settings-list'}) +@Component({ + selector: 'page-core-settings-list', + templateUrl: 'list.html', +}) +export class CoreSettingsListPage { + @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; + + handlers: CoreSettingsHandlerData[]; + isIOS: boolean; + selectedPage: string; + + constructor(private settingsDelegate: CoreSettingsDelegate, private navCtrl: NavController, platorm: Platform, + navParams: NavParams) { + this.isIOS = platorm.is('ios'); + + this.selectedPage = navParams.get('page') || false; + } + + /** + * View loaded. + */ + ionViewDidLoad(): void { + this.handlers = this.settingsDelegate.getHandlers(); + if (this.selectedPage) { + this.openHandler(this.selectedPage); + } else if (this.splitviewCtrl.isOn()) { + this.openHandler('CoreSettingsGeneralPage'); + } + + } + + /** + * Open a handler. + * + * @param {string} page Page to open. + * @param {any} params Params of the page to open. + */ + openHandler(page: string, params?: any): void { + this.selectedPage = page; + this.splitviewCtrl.push(page, params); + } + +} diff --git a/src/core/settings/providers/delegate.ts b/src/core/settings/providers/delegate.ts new file mode 100644 index 000000000..df8d2e346 --- /dev/null +++ b/src/core/settings/providers/delegate.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 { Injectable } from '@angular/core'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; + +/** + * Interface that all settings handlers must implement. + */ +export interface CoreSettingsHandler extends CoreDelegateHandler { + /** + * The highest priority is displayed first. + * @type {number} + */ + priority: number; + + /** + * Returns the data needed to render the handler. + * + * @return {CoreSettingsHandlerData} Data. + */ + getDisplayData(): CoreSettingsHandlerData; +} + +/** + * Data needed to render a setting handler. It's returned by the handler. + */ +export interface CoreSettingsHandlerData { + /** + * Name of the page to load for the handler. + * @type {string} + */ + page: string; + + /** + * Params list of the page to load for the handler. + * @type {any} + */ + params?: any; + + /** + * Title to display for the handler. + * @type {string} + */ + title: string; + + /** + * Name of the icon to display for the handler. + * @type {string} + */ + icon?: string; // Name of the icon to display in the menu. + + /** + * Class to add to the displayed handler. + * @type {string} + */ + class?: string; +} + +/** + * Service to interact with addons to be shown in app settings. Provides functions to register a plugin + * and notify an update in the data. + */ +@Injectable() +export class CoreSettingsDelegate extends CoreDelegate { + + protected handlers: { [s: string]: CoreSettingsHandler } = {}; + protected enabledHandlers: { [s: string]: CoreSettingsHandler } = {}; + protected siteHandlers: CoreSettingsHandlerData[] = []; // Handlers to return. + + constructor(protected loggerProvider: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider, + protected eventsProvider: CoreEventsProvider, protected utils: CoreUtilsProvider) { + super('CoreSettingsDelegate', loggerProvider, sitesProvider, eventsProvider); + + eventsProvider.on(CoreEventsProvider.LOGOUT, this.clearSiteHandlers.bind(this)); + } + + /** + * Clear current site handlers. Reserved for core use. + */ + clearSiteHandlers(): void { + this.siteHandlers = []; + } + + /** + * Get the handlers for the current site. + * + * @return {CoreSettingsHandlerData[]} + */ + getHandlers(): CoreSettingsHandlerData[] { + return this.siteHandlers; + } + + /** + * Update handlers Data. + */ + updateData(): void { + const handlersData: any[] = []; + + for (const name in this.enabledHandlers) { + const handler = this.enabledHandlers[name], + data = handler.getDisplayData(); + + handlersData.push({ + data: data, + priority: handler.priority + }); + } + + // Sort them by priority. + handlersData.sort((a, b) => { + return b.priority - a.priority; + }); + + // Return only the display data. + this.siteHandlers = handlersData.map((item) => { + return item.data; + }); + } +} diff --git a/src/core/settings/settings.module.ts b/src/core/settings/settings.module.ts new file mode 100644 index 000000000..91efeb1b1 --- /dev/null +++ b/src/core/settings/settings.module.ts @@ -0,0 +1,27 @@ +// (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 { CoreSettingsDelegate } from './providers/delegate'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + providers: [ + CoreSettingsDelegate + ] +}) +export class CoreSettingsModule {} diff --git a/src/core/sharedfiles/pages/choose-site/choose-site.module.ts b/src/core/sharedfiles/pages/choose-site/choose-site.module.ts index 2436ede4b..4acb401d6 100644 --- a/src/core/sharedfiles/pages/choose-site/choose-site.module.ts +++ b/src/core/sharedfiles/pages/choose-site/choose-site.module.ts @@ -16,8 +16,8 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { CoreSharedFilesChooseSitePage } from './choose-site'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreComponentsModule } from '../../../../components/components.module'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/core/sharedfiles/pages/choose-site/choose-site.ts b/src/core/sharedfiles/pages/choose-site/choose-site.ts index 86ca16621..e8cc53e08 100644 --- a/src/core/sharedfiles/pages/choose-site/choose-site.ts +++ b/src/core/sharedfiles/pages/choose-site/choose-site.ts @@ -14,9 +14,9 @@ import { Component, OnInit } from '@angular/core'; import { IonicPage, NavController, NavParams } from 'ionic-angular'; -import { CoreFileProvider } from '../../../../providers/file'; -import { CoreSitesProvider } from '../../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreFileProvider } from '@providers/file'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreSharedFilesHelperProvider } from '../../providers/helper'; /** diff --git a/src/core/sharedfiles/pages/list/list.module.ts b/src/core/sharedfiles/pages/list/list.module.ts index af48247d6..9fbe43b58 100644 --- a/src/core/sharedfiles/pages/list/list.module.ts +++ b/src/core/sharedfiles/pages/list/list.module.ts @@ -16,8 +16,8 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { CoreSharedFilesListPage } from './list'; import { TranslateModule } from '@ngx-translate/core'; -import { CoreComponentsModule } from '../../../../components/components.module'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/core/sharedfiles/pages/list/list.ts b/src/core/sharedfiles/pages/list/list.ts index c018d4175..ad3f6f136 100644 --- a/src/core/sharedfiles/pages/list/list.ts +++ b/src/core/sharedfiles/pages/list/list.ts @@ -15,10 +15,10 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { IonicPage, ViewController, NavParams, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreEventsProvider } from '../../../../providers/events'; -import { CoreFileProvider } from '../../../../providers/file'; -import { CoreSitesProvider } from '../../../../providers/sites'; -import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreFileProvider } from '@providers/file'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreSharedFilesProvider } from '../../providers/sharedfiles'; /** diff --git a/src/core/sharedfiles/providers/helper.ts b/src/core/sharedfiles/providers/helper.ts index b7307bdd7..36cbcbba7 100644 --- a/src/core/sharedfiles/providers/helper.ts +++ b/src/core/sharedfiles/providers/helper.ts @@ -15,13 +15,13 @@ import { Injectable } from '@angular/core'; import { AlertController, ModalController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreAppProvider } from '../../../providers/app'; -import { CoreFileProvider } from '../../../providers/file'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreInitDelegate } from '../../../providers/init'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; -import { CoreUtilsProvider } from '../../../providers/utils/utils'; +import { CoreAppProvider } from '@providers/app'; +import { CoreFileProvider } from '@providers/file'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreInitDelegate } from '@providers/init'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSharedFilesProvider } from './sharedfiles'; import { CoreFileUploaderProvider } from '../../fileuploader/providers/fileuploader'; diff --git a/src/core/sharedfiles/providers/sharedfiles.ts b/src/core/sharedfiles/providers/sharedfiles.ts index c34daf4d6..32e5fd716 100644 --- a/src/core/sharedfiles/providers/sharedfiles.ts +++ b/src/core/sharedfiles/providers/sharedfiles.ts @@ -13,15 +13,15 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreAppProvider } from '../../../providers/app'; -import { CoreEventsProvider } from '../../../providers/events'; -import { CoreFileProvider } from '../../../providers/file'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreMimetypeUtilsProvider } from '../../../providers/utils/mimetype'; -import { CoreTextUtilsProvider } from '../../../providers/utils/text'; +import { CoreAppProvider } from '@providers/app'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreFileProvider } from '@providers/file'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; import { Md5 } from 'ts-md5/dist/md5'; -import { SQLiteDB } from '../../../classes/sqlitedb'; +import { SQLiteDB } from '@classes/sqlitedb'; /** * Service to share files with the app. diff --git a/src/core/sitehome/components/components.module.ts b/src/core/sitehome/components/components.module.ts index 044a2fa01..9a2a04469 100644 --- a/src/core/sitehome/components/components.module.ts +++ b/src/core/sitehome/components/components.module.ts @@ -16,8 +16,8 @@ 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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; import { CoreCourseComponentsModule } from '../../course/components/components.module'; import { CoreSiteHomeIndexComponent } from './index/index'; import { CoreSiteHomeAllCourseListComponent } from './all-course-list/all-course-list'; diff --git a/src/core/sitehome/components/index/index.ts b/src/core/sitehome/components/index/index.ts index f2dadf33a..2c6140efb 100644 --- a/src/core/sitehome/components/index/index.ts +++ b/src/core/sitehome/components/index/index.ts @@ -13,8 +13,8 @@ // limitations under the License. import { Component, OnInit } from '@angular/core'; -import { CoreSitesProvider } from '../../../../providers/sites'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreCourseProvider } from '../../../course/providers/course'; import { CoreCourseHelperProvider } from '../../../course/providers/helper'; import { CoreCourseModulePrefetchDelegate } from '../../../course/providers/module-prefetch-delegate'; diff --git a/src/core/sitehome/components/news/news.ts b/src/core/sitehome/components/news/news.ts index 2e3381ac1..5f928e845 100644 --- a/src/core/sitehome/components/news/news.ts +++ b/src/core/sitehome/components/news/news.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, OnInit } from '@angular/core'; -import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreSitesProvider } from '@providers/sites'; /** * Component that displays site home news. diff --git a/src/core/sitehome/pages/index/index.ts b/src/core/sitehome/pages/index/index.ts index b2b7cea63..162ca1561 100644 --- a/src/core/sitehome/pages/index/index.ts +++ b/src/core/sitehome/pages/index/index.ts @@ -14,7 +14,7 @@ import { Component } from '@angular/core'; import { IonicPage, NavParams, NavController } from 'ionic-angular'; -import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreSitesProvider } from '@providers/sites'; import { CoreCourseHelperProvider } from '../../../course/providers/helper'; /** diff --git a/src/core/sitehome/providers/index-link-handler.ts b/src/core/sitehome/providers/index-link-handler.ts index 06eeb9cd6..12ec609cb 100644 --- a/src/core/sitehome/providers/index-link-handler.ts +++ b/src/core/sitehome/providers/index-link-handler.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreSitesProvider } from '@providers/sites'; import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; import { CoreLoginHelperProvider } from '../../login/providers/helper'; diff --git a/src/core/sitehome/providers/sitehome.ts b/src/core/sitehome/providers/sitehome.ts index 3f9e131be..036828a8a 100644 --- a/src/core/sitehome/providers/sitehome.ts +++ b/src/core/sitehome/providers/sitehome.ts @@ -13,9 +13,9 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreSite } from '../../../classes/site'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSite } from '@classes/site'; import { CoreCourseProvider } from '../../course/providers/course'; /** diff --git a/src/core/user/components/components.module.ts b/src/core/user/components/components.module.ts index 215319610..a996fbb8c 100644 --- a/src/core/user/components/components.module.ts +++ b/src/core/user/components/components.module.ts @@ -18,9 +18,9 @@ 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 { CoreComponentsModule } from '../../../components/components.module'; -import { CoreDirectivesModule } from '../../../directives/directives.module'; -import { CorePipesModule } from '../../../pipes/pipes.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives'; +import { CorePipesModule } from '@pipes'; @NgModule({ declarations: [ diff --git a/src/core/user/components/participants/participants.html b/src/core/user/components/participants/participants.html index f089dd40f..266a79210 100644 --- a/src/core/user/components/participants/participants.html +++ b/src/core/user/components/participants/participants.html @@ -10,7 +10,7 @@
- +

{{ 'core.lastaccess' | translate }}: {{ participant.lastaccess * 1000 | coreFormatDate:"dfmediumdate"}}

diff --git a/src/core/user/components/participants/participants.ts b/src/core/user/components/participants/participants.ts index 675be0144..ccc807479 100644 --- a/src/core/user/components/participants/participants.ts +++ b/src/core/user/components/participants/participants.ts @@ -15,8 +15,8 @@ import { Component, ViewChild, Input, OnInit } from '@angular/core'; import { Content, NavParams } from 'ionic-angular'; import { CoreUserProvider } from '../../providers/user'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; -import { CoreSplitViewComponent } from '../../../../components/split-view/split-view'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; /** * Component that displays the list of course participants. diff --git a/src/core/user/components/user-profile-field/user-profile-field.ts b/src/core/user/components/user-profile-field/user-profile-field.ts index 7cbd32d4f..65b401a32 100644 --- a/src/core/user/components/user-profile-field/user-profile-field.ts +++ b/src/core/user/components/user-profile-field/user-profile-field.ts @@ -14,7 +14,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { CoreUserProfileFieldDelegate } from '../../providers/user-profile-field-delegate'; -import { CoreUtilsProvider } from '../../../../providers/utils/utils'; +import { CoreUtilsProvider } from '@providers/utils/utils'; /** * Directive to render user profile field. diff --git a/src/core/user/pages/about/about.module.ts b/src/core/user/pages/about/about.module.ts index 22ea2afdd..18abe73ea 100644 --- a/src/core/user/pages/about/about.module.ts +++ b/src/core/user/pages/about/about.module.ts @@ -16,8 +16,8 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreUserAboutPage } from './about'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; -import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreDirectivesModule } from '@directives'; +import { CoreComponentsModule } from '@components/components.module'; import { CoreUserComponentsModule } from '../../components/components.module'; @NgModule({ diff --git a/src/core/user/pages/about/about.ts b/src/core/user/pages/about/about.ts index 856760407..9d9b77a05 100644 --- a/src/core/user/pages/about/about.ts +++ b/src/core/user/pages/about/about.ts @@ -16,9 +16,9 @@ import { Component } from '@angular/core'; import { IonicPage, NavParams, Platform } from 'ionic-angular'; import { CoreUserProvider } from '../../providers/user'; import { CoreUserHelperProvider } from '../../providers/helper'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; -import { CoreEventsProvider } from '../../../../providers/events'; -import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; /** * Page that displays an user about page. diff --git a/src/core/user/pages/profile/profile.html b/src/core/user/pages/profile/profile.html index ae17d5f1d..b7d1306c3 100644 --- a/src/core/user/pages/profile/profile.html +++ b/src/core/user/pages/profile/profile.html @@ -11,8 +11,7 @@
- {{ 'core.pictureof' | translate:{$a: user.fullname} }} - {{ 'core.pictureof' | translate:{$a: user.fullname} }} + {{ 'core.pictureof' | translate:{$a: user.fullname} }}

@@ -23,14 +22,16 @@

- - - -
+ + + +

{{comHandler.title | translate}}

+ + @@ -51,10 +52,10 @@ - diff --git a/src/core/user/pages/profile/profile.module.ts b/src/core/user/pages/profile/profile.module.ts index f8a6d8922..3b7e17e5c 100644 --- a/src/core/user/pages/profile/profile.module.ts +++ b/src/core/user/pages/profile/profile.module.ts @@ -16,8 +16,8 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreUserProfilePage } from './profile'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; -import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreDirectivesModule } from '@directives'; +import { CoreComponentsModule } from '@components/components.module'; @NgModule({ declarations: [ diff --git a/src/core/user/pages/profile/profile.scss b/src/core/user/pages/profile/profile.scss index ff977a68f..cb0594deb 100644 --- a/src/core/user/pages/profile/profile.scss +++ b/src/core/user/pages/profile/profile.scss @@ -6,14 +6,18 @@ page-core-user-profile { font-size: 24px; } .core-user-communication-handlers { - padding: 6px 2px; background: $list-background-color; + border-bottom: 1px solid $list-border-color; .core-user-profile-handler { background: $list-background-color; border: 0; color: $core-user-profile-communication-icons-color; + p { + margin: 0; + } + .icon { border-radius: 50%; width: 32px; @@ -27,4 +31,10 @@ page-core-user-profile { } } } + + .core-user-profile-handler { + ion-spinner { + margin-left: 0.3em; + } + } } \ No newline at end of file diff --git a/src/core/user/pages/profile/profile.ts b/src/core/user/pages/profile/profile.ts index 0c188c07c..08dd1faf7 100644 --- a/src/core/user/pages/profile/profile.ts +++ b/src/core/user/pages/profile/profile.ts @@ -16,15 +16,15 @@ import { Component, Optional } from '@angular/core'; import { IonicPage, NavParams, NavController } from 'ionic-angular'; import { CoreUserProvider } from '../../providers/user'; import { CoreUserHelperProvider } from '../../providers/helper'; -import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { TranslateService } from '@ngx-translate/core'; -import { CoreCoursesProvider } from '../../../courses/providers/courses'; -import { CoreEventsProvider } from '../../../../providers/events'; -import { CoreSitesProvider } from '../../../../providers/sites'; -import { CoreMimetypeUtilsProvider } from '../../../../providers/utils/mimetype'; -import { CoreFileUploaderHelperProvider } from '../../../fileuploader/providers/helper'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; +import { CoreFileUploaderHelperProvider } from '@core/fileuploader/providers/helper'; import { CoreUserDelegate, CoreUserProfileHandlerData } from '../../providers/user-delegate'; -import { CoreSplitViewComponent } from '../../../../components/split-view/split-view'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; /** * Page that displays an user profile page. @@ -39,6 +39,7 @@ export class CoreUserProfilePage { protected userId: number; protected site; protected obsProfileRefreshed: any; + protected subscription; userLoaded = false; isLoadingHandlers = false; @@ -102,9 +103,7 @@ export class CoreUserProfilePage { this.user = user; this.title = user.fullname; - this.isLoadingHandlers = true; - - this.userDelegate.getProfileHandlersFor(user, this.courseId).then((handlers) => { + this.subscription = this.userDelegate.getProfileHandlersFor(user, this.courseId).subscribe((handlers) => { this.actionHandlers = []; this.newPageHandlers = []; this.communicationHandlers = []; @@ -122,8 +121,8 @@ export class CoreUserProfilePage { break; } }); - }).finally(() => { - this.isLoadingHandlers = false; + + this.isLoadingHandlers = !this.userDelegate.areHandlersLoaded(); }); }).catch((error) => { @@ -208,6 +207,8 @@ export class CoreUserProfilePage { * Page destroyed. */ ngOnDestroy(): void { + this.subscription && this.subscription.unsubscribe(); this.obsProfileRefreshed && this.obsProfileRefreshed.off(); + this.userDelegate.clearUserHandlers(); } } diff --git a/src/core/user/providers/course-option-handler.ts b/src/core/user/providers/course-option-handler.ts index 95ed8173d..c595341b8 100644 --- a/src/core/user/providers/course-option-handler.ts +++ b/src/core/user/providers/course-option-handler.ts @@ -79,7 +79,7 @@ export class CoreUserParticipantsCourseOptionHandler implements CoreCourseOption /** * Returns the data needed to render the handler. * - * @return {CoreMainMenuHandlerData} Data needed to render the handler. + * @return {CoreCourseOptionsHandlerData} Data needed to render the handler. */ getDisplayData(): CoreCourseOptionsHandlerData { return { diff --git a/src/core/user/providers/helper.ts b/src/core/user/providers/helper.ts index 044c85bd6..fd9367869 100644 --- a/src/core/user/providers/helper.ts +++ b/src/core/user/providers/helper.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreLoggerProvider } from '../../../providers/logger'; +import { CoreLoggerProvider } from '@providers/logger'; import { TranslateService } from '@ngx-translate/core'; /** diff --git a/src/core/user/providers/participants-link-handler.ts b/src/core/user/providers/participants-link-handler.ts index f085458f8..2d6f1a4b7 100644 --- a/src/core/user/providers/participants-link-handler.ts +++ b/src/core/user/providers/participants-link-handler.ts @@ -13,9 +13,9 @@ // 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 { CoreLoginHelperProvider } from '../../../core/login/providers/helper'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreLoginHelperProvider } from '@core/login/providers/helper'; import { CoreUserProvider } from './user'; /** diff --git a/src/core/user/providers/user-delegate.ts b/src/core/user/providers/user-delegate.ts index 62452c596..8716bd8b6 100644 --- a/src/core/user/providers/user-delegate.ts +++ b/src/core/user/providers/user-delegate.ts @@ -14,11 +14,12 @@ import { Injectable } from '@angular/core'; import { NavController } from 'ionic-angular'; -import { CoreDelegate, CoreDelegateHandler } from '../../../classes/delegate'; -import { CoreCoursesProvider } from '../../../core/courses/providers/courses'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreEventsProvider } from '../../../providers/events'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreEventsProvider } from '@providers/events'; +import { Subject, BehaviorSubject } from 'rxjs'; /** * Interface that all user profile handlers must implement. @@ -108,6 +109,12 @@ export interface CoreUserProfileHandlerData { * Data returned by the delegate for each handler. */ export interface CoreUserProfileHandlerToDisplay { + /** + * Name of the handler. + * @type {string} + */ + name?: string; + /** * Data to display. * @type {CoreUserProfileHandlerData} @@ -138,7 +145,6 @@ export class CoreUserDelegate extends CoreDelegate { * @type {string} */ static TYPE_COMMUNICATION = 'communication'; - /** * User profile handler type for new page. * @type {string} @@ -150,13 +156,55 @@ export class CoreUserDelegate extends CoreDelegate { */ static TYPE_ACTION = 'action'; + /** + * Update handler information event. + * @type {string} + */ + static UPDATE_HANDLER_EVENT = 'CoreUserDelegate_update_handler_event'; + protected handlers: { [s: string]: CoreUserProfileHandler } = {}; protected enabledHandlers: { [s: string]: CoreUserProfileHandler } = {}; + protected observableHandlers: Subject = + new BehaviorSubject([]); + protected userHandlers: CoreUserProfileHandlerToDisplay[] = []; protected featurePrefix = '$mmUserDelegate_'; + protected loaded = false; constructor(protected loggerProvider: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider, private coursesProvider: CoreCoursesProvider, protected eventsProvider: CoreEventsProvider) { super('CoreUserDelegate', loggerProvider, sitesProvider, eventsProvider); + + eventsProvider.on(CoreUserDelegate.UPDATE_HANDLER_EVENT, (data) => { + if (data && data.handler) { + const handler = this.userHandlers.find((userHandler) => { + return userHandler.name == data.handler; + }); + if (handler) { + for (const x in data.data) { + handler.data[x] = data.data[x]; + } + this.observableHandlers.next(this.userHandlers); + } + } + }); + } + + /** + * Check if handlers are loaded. + * + * @return {boolean} True if handlers are loaded, false otherwise. + */ + areHandlersLoaded(): boolean { + return this.loaded; + } + + /** + * Clear current user handlers. + */ + clearUserHandlers(): void { + this.loaded = false; + this.userHandlers = []; + this.observableHandlers.next(this.userHandlers); } /** @@ -164,14 +212,15 @@ export class CoreUserDelegate extends CoreDelegate { * * @param {any} user The user object. * @param {number} courseId The course ID. - * @return {Promise} Resolved with the handlers. + * @return {Subject} Resolved with the handlers. */ - getProfileHandlersFor(user: any, courseId: number): Promise { - const handlers: CoreUserProfileHandlerToDisplay[] = [], - promises = []; + getProfileHandlersFor(user: any, courseId: number): Subject { + const promises = []; + + this.userHandlers = []; // Retrieve course options forcing cache. - return this.coursesProvider.getUserCourses(true).then((courses) => { + this.coursesProvider.getUserCourses(true).then((courses) => { const courseIds = courses.map((course) => { return course.id; }); @@ -188,7 +237,8 @@ export class CoreUserDelegate extends CoreDelegate { isEnabledForUser = handler.isEnabledForUser(user, courseId, navOptions, admOptions), promise = Promise.resolve(isEnabledForUser).then((enabled) => { if (enabled) { - handlers.push({ + this.userHandlers.push({ + name: name, data: handler.getDisplayData(user, courseId), priority: handler.priority, type: handler.type || CoreUserDelegate.TYPE_NEW_PAGE @@ -203,12 +253,20 @@ export class CoreUserDelegate extends CoreDelegate { } return Promise.all(promises).then(() => { - return handlers; + // Sort them by priority. + this.userHandlers.sort((a, b) => { + return b.priority - a.priority; + }); + this.loaded = true; + this.observableHandlers.next(this.userHandlers); }); }); }).catch(() => { // Never fails. - return handlers; + this.loaded = true; + this.observableHandlers.next(this.userHandlers); }); + + return this.observableHandlers; } } diff --git a/src/core/user/providers/user-handler.ts b/src/core/user/providers/user-handler.ts index e8127290f..5ddb638b2 100644 --- a/src/core/user/providers/user-handler.ts +++ b/src/core/user/providers/user-handler.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from './user-delegate'; -import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreSitesProvider } from '@providers/sites'; /** * Profile links email handler. diff --git a/src/core/user/providers/user-profile-field-delegate.ts b/src/core/user/providers/user-profile-field-delegate.ts index c3e8b7282..41c4b3d47 100644 --- a/src/core/user/providers/user-profile-field-delegate.ts +++ b/src/core/user/providers/user-profile-field-delegate.ts @@ -13,10 +13,10 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreDelegate, CoreDelegateHandler } from '../../../classes/delegate'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreEventsProvider } from '../../../providers/events'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreEventsProvider } from '@providers/events'; export interface CoreUserProfileFieldHandler extends CoreDelegateHandler { diff --git a/src/core/user/providers/user.ts b/src/core/user/providers/user.ts index b6af8f055..052caa938 100644 --- a/src/core/user/providers/user.ts +++ b/src/core/user/providers/user.ts @@ -13,10 +13,10 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreLoggerProvider } from '../../../providers/logger'; -import { CoreSite } from '../../../classes/site'; -import { CoreSitesProvider } from '../../../providers/sites'; -import { CoreUtilsProvider } from '../../../providers/utils/utils'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSite } from '@classes/site'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; /** * Service to provide user functionalities. @@ -398,11 +398,60 @@ export class CoreUserProvider { const promises = []; users.forEach((user) => { - if (typeof user.id != 'undefined') { - promises.push(this.storeUser(user.id, user.fullname, user.profileimageurl, siteId)); + if (typeof user.id != 'undefined' && !isNaN(parseInt(user.id, 10))) { + promises.push(this.storeUser(parseInt(user.id, 10), user.fullname, user.profileimageurl, siteId)); } }); return Promise.all(promises); } + + /** + * Update a preference for a user. + * + * @param {string} name Preference name. + * @param {any} value Preference new value. + * @param {number} [userId] User ID. If not defined, site's current user. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if success. + */ + updateUserPreference(name: string, value: any, userId?: number, siteId?: string): Promise { + const preferences = [ + { + type: name, + value: value + } + ]; + + return this.updateUserPreferences(preferences, undefined, userId, siteId); + } + + /** + * Update some preferences for a user. + * + * @param {any} preferences List of preferences. + * @param {boolean} [disableNotifications] Whether to disable all notifications. Undefined to not update this value. + * @param {number} [userId] User ID. If not defined, site's current user. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if success. + */ + updateUserPreferences(preferences: any, disableNotifications: boolean, userId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + const data = { + userid: userId, + preferences: preferences + }, + preSets = { + responseExpected: false + }; + + if (typeof disableNotifications != 'undefined') { + data['emailstop'] = disableNotifications ? 1 : 0; + } + + return site.write('core_user_update_user_preferences', data, preSets); + }); + } } diff --git a/src/core/user/user.module.ts b/src/core/user/user.module.ts index 92588ca57..bfec54d89 100644 --- a/src/core/user/user.module.ts +++ b/src/core/user/user.module.ts @@ -18,13 +18,13 @@ import { CoreUserProfileFieldDelegate } from './providers/user-profile-field-del import { CoreUserProvider } from './providers/user'; import { CoreUserHelperProvider } from './providers/helper'; import { CoreUserProfileMailHandler } from './providers/user-handler'; -import { CoreEventsProvider } from '../../providers/events'; -import { CoreSitesProvider } from '../../providers/sites'; -import { CoreContentLinksDelegate } from '../contentlinks/providers/delegate'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; import { CoreUserProfileLinkHandler } from './providers/user-link-handler'; import { CoreUserParticipantsCourseOptionHandler } from './providers/course-option-handler'; import { CoreUserParticipantsLinkHandler } from './providers/participants-link-handler'; -import { CoreCourseOptionsDelegate } from '../course/providers/options-delegate'; +import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; import { CoreUserComponentsModule } from './components/components.module'; @NgModule({ diff --git a/src/core/viewer/pages/iframe/iframe.module.ts b/src/core/viewer/pages/iframe/iframe.module.ts index f5cd9ddf4..d2dab2f6b 100644 --- a/src/core/viewer/pages/iframe/iframe.module.ts +++ b/src/core/viewer/pages/iframe/iframe.module.ts @@ -15,7 +15,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { CoreViewerIframePage } from './iframe'; -import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreComponentsModule } from '@components/components.module'; @NgModule({ declarations: [ diff --git a/src/core/viewer/pages/image/image.module.ts b/src/core/viewer/pages/image/image.module.ts index 62cd6dff0..1f1a6f8e6 100644 --- a/src/core/viewer/pages/image/image.module.ts +++ b/src/core/viewer/pages/image/image.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreViewerImagePage } from './image'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreDirectivesModule } from '@directives'; @NgModule({ declarations: [ diff --git a/src/core/viewer/pages/text/text.module.ts b/src/core/viewer/pages/text/text.module.ts index 01fc3cf39..2a951b7d3 100644 --- a/src/core/viewer/pages/text/text.module.ts +++ b/src/core/viewer/pages/text/text.module.ts @@ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreViewerTextPage } from './text'; -import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreDirectivesModule } from '@directives'; /** * Module to lazy load the page. diff --git a/src/core/viewer/pages/text/text.ts b/src/core/viewer/pages/text/text.ts index 64da6c45f..98d1bf6ad 100644 --- a/src/core/viewer/pages/text/text.ts +++ b/src/core/viewer/pages/text/text.ts @@ -14,7 +14,7 @@ import { Component } from '@angular/core'; import { IonicPage, ViewController, NavParams } from 'ionic-angular'; -import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; /** * Page to render a certain text. If opened as a modal, it will have a button to close the modal. diff --git a/src/directives/auto-focus.ts b/src/directives/auto-focus.ts index 98988335a..ce2aa6608 100644 --- a/src/directives/auto-focus.ts +++ b/src/directives/auto-focus.ts @@ -14,8 +14,8 @@ import { Directive, Input, OnInit, ElementRef } from '@angular/core'; import { NavController } from 'ionic-angular'; -import { CoreDomUtilsProvider } from '../providers/utils/dom'; -import { CoreUtilsProvider } from '../providers/utils/utils'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; /** * Directive to auto focus an element when a view is loaded. diff --git a/src/directives/auto-rows.ts b/src/directives/auto-rows.ts new file mode 100644 index 000000000..39301b339 --- /dev/null +++ b/src/directives/auto-rows.ts @@ -0,0 +1,72 @@ +// (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 { Directive, ElementRef, HostListener, Output, EventEmitter } from '@angular/core'; + +/** + * Directive to adapt a textarea rows depending on the input text. It's based on Moodle's data-auto-rows. + * + * @description + * Usage: + * + */ +@Directive({ + selector: 'textarea[core-auto-rows]' +}) +export class CoreAutoRowsDirective { + protected element: HTMLTextAreaElement; + protected height = 0; + + @Output() onResize: EventEmitter; // Emit when resizing the textarea. + + constructor(element: ElementRef) { + this.element = element.nativeElement || element; + this.height = this.element.scrollHeight; + this.onResize = new EventEmitter(); + } + + @HostListener('input') onInput(): void { + this.resize(); + } + + @HostListener('change') onChange(): void { + // Fired on reset. Wait to the change to be finished. + setTimeout(() => { + this.resize(); + }, 300); + } + + /** + * Resize after init. + */ + ngAfterViewInit(): void { + this.resize(); + } + + /** + * Resize the textarea. + * @param {any} $event Event fired. + */ + protected resize($event?: any): void { + // Set height to 1px to force scroll height to calculate correctly. + this.element.style.height = '1px'; + this.element.style.height = this.element.scrollHeight + 'px'; + + // Emit event when resizing. + if (this.height != this.element.scrollHeight) { + this.height = this.element.scrollHeight; + this.onResize.emit(); + } + } +} diff --git a/src/directives/directives.module.ts b/src/directives/directives.module.ts index 11d708992..f28c00562 100644 --- a/src/directives/directives.module.ts +++ b/src/directives/directives.module.ts @@ -19,6 +19,8 @@ import { CoreFormatTextDirective } from './format-text'; import { CoreLinkDirective } from './link'; import { CoreKeepKeyboardDirective } from './keep-keyboard'; import { CoreUserLinkDirective } from './user-link'; +import { CoreAutoRowsDirective } from './auto-rows'; +import { CoreLongPressDirective } from './long-press'; @NgModule({ declarations: [ @@ -27,7 +29,9 @@ import { CoreUserLinkDirective } from './user-link'; CoreFormatTextDirective, CoreKeepKeyboardDirective, CoreLinkDirective, - CoreUserLinkDirective + CoreUserLinkDirective, + CoreAutoRowsDirective, + CoreLongPressDirective ], imports: [], exports: [ @@ -36,7 +40,9 @@ import { CoreUserLinkDirective } from './user-link'; CoreFormatTextDirective, CoreKeepKeyboardDirective, CoreLinkDirective, - CoreUserLinkDirective + CoreUserLinkDirective, + CoreAutoRowsDirective, + CoreLongPressDirective ] }) export class CoreDirectivesModule {} diff --git a/src/directives/external-content.ts b/src/directives/external-content.ts index b6e89722b..e57585817 100644 --- a/src/directives/external-content.ts +++ b/src/directives/external-content.ts @@ -14,12 +14,12 @@ import { Directive, Input, AfterViewInit, ElementRef } from '@angular/core'; import { Platform } from 'ionic-angular'; -import { CoreAppProvider } from '../providers/app'; -import { CoreLoggerProvider } from '../providers/logger'; -import { CoreFilepoolProvider } from '../providers/filepool'; -import { CoreSitesProvider } from '../providers/sites'; -import { CoreDomUtilsProvider } from '../providers/utils/dom'; -import { CoreUrlUtilsProvider } from '../providers/utils/url'; +import { CoreAppProvider } from '@providers/app'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; /** * Directive to handle external content. diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index d48e0f6a8..b65a5107d 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -15,18 +15,18 @@ import { Directive, ElementRef, Input, Output, EventEmitter, OnChanges, SimpleChange, Optional } from '@angular/core'; import { Platform, NavController, Content } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreAppProvider } from '../providers/app'; -import { CoreFilepoolProvider } from '../providers/filepool'; -import { CoreLoggerProvider } from '../providers/logger'; -import { CoreSitesProvider } from '../providers/sites'; -import { CoreDomUtilsProvider } from '../providers/utils/dom'; -import { CoreTextUtilsProvider } from '../providers/utils/text'; -import { CoreUrlUtilsProvider } from '../providers/utils/url'; -import { CoreUtilsProvider } from '../providers/utils/utils'; -import { CoreSite } from '../classes/site'; +import { CoreAppProvider } from '@providers/app'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreSite } from '@classes/site'; import { CoreLinkDirective } from '../directives/link'; import { CoreExternalContentDirective } from '../directives/external-content'; -import { CoreContentLinksHelperProvider } from '../core/contentlinks/providers/helper'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; /** * Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective diff --git a/src/directives/keep-keyboard.ts b/src/directives/keep-keyboard.ts index f953020a7..2cdc41e1f 100644 --- a/src/directives/keep-keyboard.ts +++ b/src/directives/keep-keyboard.ts @@ -13,8 +13,8 @@ // limitations under the License. import { Directive, AfterViewInit, Input, ElementRef, OnDestroy } from '@angular/core'; -import { CoreDomUtilsProvider } from '../providers/utils/dom'; -import { CoreUtilsProvider } from '../providers/utils/utils'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; /** * Directive to keep the keyboard open when clicking a certain element (usually a button). diff --git a/src/directives/link.ts b/src/directives/link.ts index 2069ce479..d37179cf0 100644 --- a/src/directives/link.ts +++ b/src/directives/link.ts @@ -14,11 +14,11 @@ import { Directive, Input, OnInit, ElementRef, Optional } from '@angular/core'; import { NavController, Content } from 'ionic-angular'; -import { CoreSitesProvider } from '../providers/sites'; -import { CoreDomUtilsProvider } from '../providers/utils/dom'; -import { CoreUrlUtilsProvider } from '../providers/utils/url'; -import { CoreUtilsProvider } from '../providers/utils/utils'; -import { CoreContentLinksHelperProvider } from '../core/contentlinks/providers/helper'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreConfigConstants } from '../configconstants'; /** diff --git a/src/directives/long-press.ts b/src/directives/long-press.ts new file mode 100644 index 000000000..2b6e84569 --- /dev/null +++ b/src/directives/long-press.ts @@ -0,0 +1,54 @@ +// (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. + +// Based on http://roblouie.com/article/198/using-gestures-in-the-ionic-2-beta/ + +import { Directive, ElementRef, OnInit, OnDestroy, Output, EventEmitter } from '@angular/core'; +import { Gesture } from 'ionic-angular/gestures/gesture'; + +/** + * Directive to add long press actions to html elements. + */ +@Directive({ + selector: '[longPress]' +}) +export class CoreLongPressDirective implements OnInit, OnDestroy { + el: HTMLElement; + pressGesture: Gesture; + + @Output() longPress = new EventEmitter(); + + constructor(el: ElementRef) { + this.el = el.nativeElement; + this.el.setAttribute('tappable', ''); + } + + /** + * Initialize gesture listening. + */ + ngOnInit(): void { + this.pressGesture = new Gesture(this.el); + this.pressGesture.listen(); + this.pressGesture.on('press', (e) => { + this.longPress.emit(e); + }); + } + + /** + * Destroy gesture listening. + */ + ngOnDestroy(): void { + this.pressGesture.destroy(); + } +} diff --git a/src/pipes/bytes-to-size.ts b/src/pipes/bytes-to-size.ts index de73241fe..6e2d07127 100644 --- a/src/pipes/bytes-to-size.ts +++ b/src/pipes/bytes-to-size.ts @@ -13,8 +13,8 @@ // limitations under the License. import { Pipe, PipeTransform } from '@angular/core'; -import { CoreLoggerProvider } from '../providers/logger'; -import { CoreTextUtilsProvider } from '../providers/utils/text'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; /** * Pipe to turn a number in bytes to a human readable size (e.g. 5,25 MB). diff --git a/src/pipes/date-day-or-time.ts b/src/pipes/date-day-or-time.ts index 11d741285..ff9607410 100644 --- a/src/pipes/date-day-or-time.ts +++ b/src/pipes/date-day-or-time.ts @@ -14,7 +14,7 @@ import { Pipe, PipeTransform } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { CoreLoggerProvider } from '../providers/logger'; +import { CoreLoggerProvider } from '@providers/logger'; import * as moment from 'moment'; /** @@ -59,9 +59,10 @@ export class CoreDateDayOrTimePipe implements PipeTransform { } return moment(timestamp * 1000).calendar(null, { - sameDay: this.translate.instant('core.dftimedate'), + sameDay: 'LT', lastDay: this.translate.instant('core.dflastweekdate'), - lastWeek: this.translate.instant('core.dflastweekdate') + lastWeek: this.translate.instant('core.dflastweekdate'), + sameElse: 'L' }); } } diff --git a/src/pipes/duration.ts b/src/pipes/duration.ts index 71ce2d1ce..cf6f615c4 100644 --- a/src/pipes/duration.ts +++ b/src/pipes/duration.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Pipe, PipeTransform } from '@angular/core'; -import { CoreLoggerProvider } from '../providers/logger'; +import { CoreLoggerProvider } from '@providers/logger'; import * as moment from 'moment'; /** diff --git a/src/pipes/format-date.ts b/src/pipes/format-date.ts index acac49f9e..e1b238fd4 100644 --- a/src/pipes/format-date.ts +++ b/src/pipes/format-date.ts @@ -14,7 +14,7 @@ import { Pipe, PipeTransform } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { CoreLoggerProvider } from '../providers/logger'; +import { CoreLoggerProvider } from '@providers/logger'; import * as moment from 'moment'; /** @@ -52,10 +52,12 @@ export class CoreFormatDatePipe implements PipeTransform { timestamp = numberTimestamp; } - if (format.indexOf('.') == -1) { - format = 'core.' + format; + if (format.indexOf('df') == 0) { + format = this.translate.instant('core.' + format); + } else if (format.indexOf('.') > 0) { + format = this.translate.instant(format); } - return moment(timestamp).format(this.translate.instant(format)); + return moment(timestamp).format(format); } } diff --git a/src/pipes/seconds-to-hms.ts b/src/pipes/seconds-to-hms.ts index 05f7825f7..dd71624d6 100644 --- a/src/pipes/seconds-to-hms.ts +++ b/src/pipes/seconds-to-hms.ts @@ -13,9 +13,9 @@ // limitations under the License. import { Pipe, PipeTransform } from '@angular/core'; -import { CoreLoggerProvider } from '../providers/logger'; -import { CoreTextUtilsProvider } from '../providers/utils/text'; -import { CoreConstants } from '../core/constants'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreConstants } from '@core/constants'; /** * Pipe to convert a number of seconds to Hours:Minutes:Seconds. diff --git a/src/pipes/time-ago.ts b/src/pipes/time-ago.ts index d0283e9ec..07f8d3dd6 100644 --- a/src/pipes/time-ago.ts +++ b/src/pipes/time-ago.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Pipe, PipeTransform } from '@angular/core'; -import { CoreLoggerProvider } from '../providers/logger'; +import { CoreLoggerProvider } from '@providers/logger'; import * as moment from 'moment'; /** diff --git a/src/pipes/to-locale-string.ts b/src/pipes/to-locale-string.ts index 84f880054..19c3434fb 100644 --- a/src/pipes/to-locale-string.ts +++ b/src/pipes/to-locale-string.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Pipe, PipeTransform } from '@angular/core'; -import { CoreLoggerProvider } from '../providers/logger'; +import { CoreLoggerProvider } from '@providers/logger'; /** * Filter to format a timestamp to a locale string. Timestamp can be in seconds or milliseconds. diff --git a/src/providers/app.ts b/src/providers/app.ts index 054ef2467..1cac19e4b 100644 --- a/src/providers/app.ts +++ b/src/providers/app.ts @@ -19,7 +19,7 @@ import { Network } from '@ionic-native/network'; import { CoreDbProvider } from './db'; import { CoreLoggerProvider } from './logger'; -import { SQLiteDB } from '../classes/sqlitedb'; +import { SQLiteDB } from '@classes/sqlitedb'; /** * Data stored for a redirect to another page/site. diff --git a/src/providers/config.ts b/src/providers/config.ts index 1898d189d..554ee3b21 100644 --- a/src/providers/config.ts +++ b/src/providers/config.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreAppProvider } from './app'; -import { SQLiteDB } from '../classes/sqlitedb'; +import { SQLiteDB } from '@classes/sqlitedb'; /** * Factory to provide access to dynamic and permanent config and settings. @@ -67,9 +67,9 @@ export class CoreConfigProvider { }).catch((error) => { if (typeof defaultValue != 'undefined') { return defaultValue; - } else { - return Promise.reject(error); } + + return Promise.reject(error); }); } diff --git a/src/providers/cron.ts b/src/providers/cron.ts index a33af21b2..930d324a2 100644 --- a/src/providers/cron.ts +++ b/src/providers/cron.ts @@ -18,8 +18,8 @@ import { CoreAppProvider } from './app'; import { CoreConfigProvider } from './config'; import { CoreLoggerProvider } from './logger'; import { CoreUtilsProvider } from './utils/utils'; -import { CoreConstants } from '../core/constants'; -import { SQLiteDB } from '../classes/sqlitedb'; +import { CoreConstants } from '@core/constants'; +import { SQLiteDB } from '@classes/sqlitedb'; /** * Interface that all cron handlers must implement. @@ -72,13 +72,13 @@ export interface CoreCronHandler { * Whether the handler is running. Used internally by the provider, there's no need to set it. * @type {boolean} */ - running: boolean; + running?: boolean; /** * Timeout ID for the handler scheduling. Used internally by the provider, there's no need to set it. * @type {number} */ - timeout: number; + timeout?: number; } /* @@ -387,18 +387,18 @@ export class CoreCronDelegate { return; } if (typeof this.handlers[handler.name] != 'undefined') { - this.logger.debug(`The cron handler '${name}' is already registered.`); + this.logger.debug(`The cron handler '${handler.name}' is already registered.`); return; } - this.logger.debug.debug(`Register handler '${name}' in cron.`); + this.logger.debug(`Register handler '${handler.name}' in cron.`); handler.running = false; this.handlers[handler.name] = handler; // Start the handler. - this.startHandler(name); + this.startHandler(handler.name); } /** diff --git a/src/providers/db.ts b/src/providers/db.ts index fa4fd6192..e1007fa27 100644 --- a/src/providers/db.ts +++ b/src/providers/db.ts @@ -15,8 +15,8 @@ import { Injectable } from '@angular/core'; import { SQLite } from '@ionic-native/sqlite'; import { Platform } from 'ionic-angular'; -import { SQLiteDB } from '../classes/sqlitedb'; -import { SQLiteDBMock } from '../core/emulator/classes/sqlitedb'; +import { SQLiteDB } from '@classes/sqlitedb'; +import { SQLiteDBMock } from '@core/emulator/classes/sqlitedb'; /** * This service allows interacting with the local database to store and retrieve data. diff --git a/src/providers/events.ts b/src/providers/events.ts index 5ca5c2ab1..bbe557636 100644 --- a/src/providers/events.ts +++ b/src/providers/events.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { Subject } from 'rxjs'; -import { CoreLoggerProvider } from '../providers/logger'; +import { CoreLoggerProvider } from '@providers/logger'; /** * Observer instance to stop listening to an event. diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts index ab2dba3e6..cc677fef9 100644 --- a/src/providers/filepool.ts +++ b/src/providers/filepool.ts @@ -27,8 +27,8 @@ import { CoreTextUtilsProvider } from './utils/text'; import { CoreTimeUtilsProvider } from './utils/time'; import { CoreUrlUtilsProvider } from './utils/url'; import { CoreUtilsProvider } from './utils/utils'; -import { SQLiteDB } from '../classes/sqlitedb'; -import { CoreConstants } from '../core/constants'; +import { SQLiteDB } from '@classes/sqlitedb'; +import { CoreConstants } from '@core/constants'; import { Md5 } from 'ts-md5/dist/md5'; /** diff --git a/src/providers/local-notifications.ts b/src/providers/local-notifications.ts index 894d228e3..4bd55cfac 100644 --- a/src/providers/local-notifications.ts +++ b/src/providers/local-notifications.ts @@ -20,8 +20,8 @@ import { CoreConfigProvider } from './config'; import { CoreLoggerProvider } from './logger'; import { CoreDomUtilsProvider } from './utils/dom'; import { CoreUtilsProvider } from './utils/utils'; -import { SQLiteDB } from '../classes/sqlitedb'; -import { CoreConstants } from '../core/constants'; +import { SQLiteDB } from '@classes/sqlitedb'; +import { CoreConstants } from '@core/constants'; import { Subject } from 'rxjs'; /** @@ -42,10 +42,13 @@ export interface CoreILocalNotification extends ILocalNotification { } /* - Generated class for the LocalNotificationsProvider provider. - - See https://angular.io/guide/dependency-injection for more info on providers - and Angular DI. + * Generated class for the LocalNotificationsProvider provider. + * + * 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 { diff --git a/src/providers/sites-factory.ts b/src/providers/sites-factory.ts index e28519e1e..dda512f8c 100644 --- a/src/providers/sites-factory.ts +++ b/src/providers/sites-factory.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; -import { CoreSite } from '../classes/site'; +import { CoreSite } from '@classes/site'; /* * Provider to create sites instances. diff --git a/src/providers/sites.ts b/src/providers/sites.ts index bcefaba14..435024ee0 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -21,10 +21,10 @@ import { CoreLoggerProvider } from './logger'; import { CoreSitesFactoryProvider } from './sites-factory'; import { CoreUrlUtilsProvider } from './utils/url'; import { CoreUtilsProvider } from './utils/utils'; -import { CoreConstants } from '../core/constants'; +import { CoreConstants } from '@core/constants'; import { CoreConfigConstants } from '../configconstants'; -import { CoreSite } from '../classes/site'; -import { SQLiteDB } from '../classes/sqlitedb'; +import { CoreSite } from '@classes/site'; +import { SQLiteDB } from '@classes/sqlitedb'; import { Md5 } from 'ts-md5/dist/md5'; /** @@ -744,29 +744,14 @@ export class CoreSitesProvider { }); } - /** - * Check if there are no sites stored. - * - * @return {Promise} Promise resolved if there are no sites, and rejected if there is at least one. - */ - hasNoSites(): Promise { - return this.appDB.countRecords(this.SITES_TABLE).then((count) => { - if (count > 0) { - return Promise.reject(null); - } - }); - } - /** * Check if there are sites stored. * - * @return {Promise} Promise resolved if there is at least one site, and rejected if there aren't. + * @return {Promise} Promise resolved with true if there are sites and false if there aren't. */ - hasSites(): Promise { + hasSites(): Promise { return this.appDB.countRecords(this.SITES_TABLE).then((count) => { - if (count == 0) { - return Promise.reject(null); - } + return count > 0; }); } @@ -1212,4 +1197,17 @@ export class CoreSitesProvider { this.sites[id].getDb().createTablesFromSchema(tables); } } + + /** + * Check if a WS is available in the current site, if any. + * + * @param {string} method WS name. + * @param {boolean} [checkPrefix=true] When true also checks with the compatibility prefix. + * @return {boolean} Whether the WS is available. + */ + wsAvailableInCurrentSite(method: string, checkPrefix: boolean = true): boolean { + const site = this.getCurrentSite(); + + return site && site.wsAvailable(method, checkPrefix); + } } diff --git a/src/providers/sync.ts b/src/providers/sync.ts index ea8d6ee75..2003b488d 100644 --- a/src/providers/sync.ts +++ b/src/providers/sync.ts @@ -23,9 +23,9 @@ import { CoreSitesProvider } from './sites'; export class CoreSyncProvider { // Variables for the database. - static SYNC_TABLE = 'sync'; + protected SYNC_TABLE = 'sync'; protected tableSchema = { - name: CoreSyncProvider.SYNC_TABLE, + name: this.SYNC_TABLE, columns: [ { name: 'component', @@ -34,7 +34,7 @@ export class CoreSyncProvider { }, { name: 'id', - type: 'INTEGER', + type: 'TEXT', notNull: true }, { @@ -116,6 +116,36 @@ export class CoreSyncProvider { } } + /** + * Returns a sync record. + * @param {string} component Component name. + * @param {string | number} id Unique ID per component. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Record if found or reject. + */ + getSyncRecord(component: string, id: string | number, siteId?: string): Promise { + return this.sitesProvider.getSiteDb(siteId).then((db) => { + return db.getRecord(this.SYNC_TABLE, { component: component, id: id }); + }); + } + + /** + * Inserts or Updates info of a sync record. + * @param {string} component Component name. + * @param {string | number} id Unique ID per component. + * @param {any} data Data that updates the record. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with done. + */ + insertOrUpdateSyncRecord(component: string, id: string | number, data: any, siteId?: string): Promise { + return this.sitesProvider.getSiteDb(siteId).then((db) => { + data.component = component; + data.id = id; + + return db.insertOrUpdateRecord(this.SYNC_TABLE, data, { component: component, id: id }); + }); + } + /** * Convenience function to create unique identifiers for a component and id. * diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index 7706c15fd..72843064c 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -22,7 +22,7 @@ import { CoreTextUtilsProvider } from './text'; import { CoreAppProvider } from '../app'; import { CoreConfigProvider } from '../config'; import { CoreUrlUtilsProvider } from './url'; -import { CoreConstants } from '../../core/constants'; +import { CoreConstants } from '@core/constants'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -152,7 +152,7 @@ export class CoreDomUtilsProvider { this.element.innerHTML = html; elements = this.element.querySelectorAll('a, img, audio, video, source, track'); - for (const i in elements) { + for (let i = 0; i < elements.length; i++) { const element = elements[i]; let url = element.tagName === 'A' ? element.href : element.src; @@ -480,11 +480,19 @@ export class CoreDomUtilsProvider { * * @param {HTMLElement} oldParent The old parent. * @param {HTMLElement} newParent The new parent. + * @return {Node[]} List of moved children. */ - moveChildren(oldParent: HTMLElement, newParent: HTMLElement): void { + moveChildren(oldParent: HTMLElement, newParent: HTMLElement): Node[] { + const movedChildren: Node[] = []; + while (oldParent.childNodes.length > 0) { - newParent.appendChild(oldParent.childNodes[0]); + const child = oldParent.childNodes[0]; + movedChildren.push(child); + + newParent.appendChild(child); } + + return movedChildren; } /** @@ -517,7 +525,7 @@ export class CoreDomUtilsProvider { if (removeAll) { selected = this.element.querySelectorAll(selector); - for (const i in selected) { + for (let i = 0; i < selected.length; i++) { selected[i].remove(); } } else { @@ -560,7 +568,7 @@ export class CoreDomUtilsProvider { for (const key in map) { const foundElements = element.querySelectorAll('.' + key); - for (const i in foundElements) { + for (let i = 0; i < foundElements.length; i++) { const foundElement = foundElements[i]; foundElement.className = foundElement.className.replace(key, map[key]); } diff --git a/src/providers/utils/time.ts b/src/providers/utils/time.ts index 2cdd4e771..8ab41f1f9 100644 --- a/src/providers/utils/time.ts +++ b/src/providers/utils/time.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import * as moment from 'moment'; -import { CoreConstants } from '../../core/constants'; +import { CoreConstants } from '@core/constants'; /* * "Utils" service with helper functions for date and time. diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index e1bd4a08c..ec57e20d4 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -23,6 +23,7 @@ import { CoreEventsProvider } from '../events'; import { CoreLoggerProvider } from '../logger'; import { TranslateService } from '@ngx-translate/core'; import { CoreLangProvider } from '../lang'; +import { CoreWSError } from '../ws'; /** * Deferred promise. It's similar to the result of $q.defer() in AngularJS. @@ -601,28 +602,31 @@ export class CoreUtilsProvider { return typeof value != 'undefined' && (value === true || value === 'true' || parseInt(value, 10) === 1); } + /** + * Create a "fake" WS error for local errors. + * + * @param {string} message The message to include in the error. + * @param {boolean} [needsTranslate] If the message needs to be translated. + * @return {CoreWSError} Fake WS error. + */ + createFakeWSError(message: string, needsTranslate?: boolean): CoreWSError { + if (needsTranslate) { + message = this.translate.instant(message); + } + + return { + message: message + }; + } + /** * Given an error returned by a WS call, check if the error is generated by the app or it has been returned by the WebSwervice. * - * @param {string} error Error to check. + * @param {any} error Error to check. * @return {boolean} Whether the error was returned by the WebService. */ - isWebServiceError(error: string): boolean { - const localErrors = [ - this.translate.instant('core.wsfunctionnotavailable'), - this.translate.instant('core.lostconnection'), - this.translate.instant('core.userdeleted'), - this.translate.instant('core.unexpectederror'), - this.translate.instant('core.networkerrormsg'), - this.translate.instant('core.serverconnection'), - this.translate.instant('core.errorinvalidresponse'), - this.translate.instant('core.sitemaintenance'), - this.translate.instant('core.upgraderunning'), - this.translate.instant('core.nopasswordchangeforced'), - this.translate.instant('core.unicodenotsupported') - ]; - - return error && localErrors.indexOf(error) == -1; + isWebServiceError(error: any): boolean { + return typeof error.errorcode == 'undefined'; } /** diff --git a/src/providers/ws.ts b/src/providers/ws.ts index dac52324d..462678683 100644 --- a/src/providers/ws.ts +++ b/src/providers/ws.ts @@ -23,9 +23,9 @@ import { CoreLoggerProvider } from './logger'; import { CoreMimetypeUtilsProvider } from './utils/mimetype'; import { CoreTextUtilsProvider } from './utils/text'; import { CoreUtilsProvider } from './utils/utils'; -import { CoreConstants } from '../core/constants'; +import { CoreConstants } from '@core/constants'; import { Md5 } from 'ts-md5/dist/md5'; -import { CoreInterceptor } from '../classes/interceptor'; +import { CoreInterceptor } from '@classes/interceptor'; /** * PreSets accepted by the WS call. @@ -174,9 +174,9 @@ export class CoreWSProvider { let siteUrl; if (!preSets) { - return Promise.reject(this.createFakeWSError('core.unexpectederror', true)); + return Promise.reject(this.utils.createFakeWSError('core.unexpectederror', true)); } else if (!this.appProvider.isOnline()) { - return Promise.reject(this.createFakeWSError('core.networkerrormsg', true)); + return Promise.reject(this.utils.createFakeWSError('core.networkerrormsg', true)); } preSets.typeExpected = preSets.typeExpected || 'object'; @@ -320,23 +320,6 @@ export class CoreWSProvider { return result; } - /** - * Create a "fake" WS error for local errors. - * - * @param {string} message The message to include in the error. - * @param {boolean} [needsTranslate] If the message needs to be translated. - * @return {CoreWSError} Fake WS error. - */ - createFakeWSError(message: string, needsTranslate?: boolean): CoreWSError { - if (needsTranslate) { - message = this.translate.instant(message); - } - - return { - message: message - }; - } - /** * Downloads a file from Moodle using Cordova File API. * @@ -532,11 +515,11 @@ export class CoreWSProvider { } if (!data) { - return Promise.reject(this.createFakeWSError('core.serverconnection', true)); + return Promise.reject(this.utils.createFakeWSError('core.serverconnection', true)); } else if (typeof data != preSets.typeExpected) { this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`); - return Promise.reject(this.createFakeWSError('core.errorinvalidresponse', true)); + return Promise.reject(this.utils.createFakeWSError('core.errorinvalidresponse', true)); } if (typeof data.exception !== 'undefined') { @@ -544,7 +527,7 @@ export class CoreWSProvider { } if (typeof data.debuginfo != 'undefined') { - return Promise.reject(this.createFakeWSError('Error. ' + data.message)); + return Promise.reject(this.utils.createFakeWSError('Error. ' + data.message)); } return data; @@ -572,7 +555,7 @@ export class CoreWSProvider { return retryPromise; } - return Promise.reject(this.createFakeWSError('core.serverconnection', true)); + return Promise.reject(this.utils.createFakeWSError('core.serverconnection', true)); }); promise = this.setPromiseHttp(promise, 'post', preSets.siteUrl, ajaxData); diff --git a/src/theme/variables.scss b/src/theme/variables.scss index 596cd09b9..9f6fc3d6b 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -50,7 +50,6 @@ $core-color: $orange; $core-color-light: lighten($core-color, 10%); $core-color-dark: darken($core-color, 10%); - // Shared Variables // -------------------------------------------------- // To customize the look and feel of this app, you can override @@ -204,3 +203,17 @@ $core-top-tabs-border: $gray; $core-top-tabs-color-active: $core-color; $core-user-profile-communication-icons-color: $core-color; + +$core-rte-min-height: 80px; + +$core-toolbar-button-image-width: 32px; + +// Mixins +// ------------------------- +@mixin core-transition($where: all, $time: 500ms) { + -webkit-transition: $where $time ease-in-out; + -moz-transition: $where $time ease-in-out; + -ms-transition: $where $time ease-in-out; + -o-transition: $where $time ease-in-out; + transition: $where $time ease-in-out; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 2e450f9f1..a6feed9c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,17 @@ "module": "es2015", "moduleResolution": "node", "sourceMap": true, - "target": "es5" + "target": "es5", + "baseUrl": "./src", + "paths": { + "@addon/*": ["addon/*"], + "@classes/*": ["classes/*"], + "@core/*": ["core/*"], + "@providers/*": ["providers/*"], + "@components/*": ["components/*"], + "@directives": ["directives/directives.module"], + "@pipes": ["pipes/pipes.module"] + } }, "include": [ "src/**/*.ts" @@ -23,4 +33,4 @@ "atom": { "rewriteTsconfig": false } -} \ No newline at end of file +}