diff --git a/src/addon/messages/messages.module.ts b/src/addon/messages/messages.module.ts index ad9499a94..779d1c8f9 100644 --- a/src/addon/messages/messages.module.ts +++ b/src/addon/messages/messages.module.ts @@ -56,6 +56,7 @@ export class AddonMessagesModule { contentLinksDelegate.registerHandler(discussionLinkHandler); userDelegate.registerHandler(sendMessageHandler); cronDelegate.register(syncHandler); + cronDelegate.register(mainmenuHandler); // Sync some discussions when device goes online. network.onConnect().subscribe(() => { diff --git a/src/addon/messages/providers/mainmenu-handler.ts b/src/addon/messages/providers/mainmenu-handler.ts index 3bd98ae39..a5bbc435d 100644 --- a/src/addon/messages/providers/mainmenu-handler.ts +++ b/src/addon/messages/providers/mainmenu-handler.ts @@ -14,17 +14,43 @@ import { Injectable } from '@angular/core'; import { AddonMessagesProvider } from './messages'; -import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../../core/mainmenu/providers/delegate'; +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 { CoreLocalNotificationsProvider } from '../../../providers/local-notifications'; /** * Handler to inject an option into main menu. */ @Injectable() -export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler { +export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCronHandler { name = 'AddonMessages'; priority = 600; + protected badge = ''; + protected loading = true; - constructor(private messagesProvider: AddonMessagesProvider) { } + constructor(private messagesProvider: AddonMessagesProvider, private sitesProvider: CoreSitesProvider, + private eventsProvider: CoreEventsProvider, private appProvider: CoreAppProvider, + private localNotificationsProvider: CoreLocalNotificationsProvider, private textUtils: CoreTextUtilsProvider) { + + 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; + }); + } /** * Check if the handler is enabled on a site level. @@ -38,14 +64,133 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler { /** * Returns the data needed to render the handler. * - * @return {CoreMainMenuHandlerData} Data needed to render the handler. + * @return {CoreMainMenuHandlerToDisplay} Data needed to render the handler. */ - getDisplayData(): CoreMainMenuHandlerData { + getDisplayData(): CoreMainMenuHandlerToDisplay { + if (this.loading) { + this.updateBadge(); + } + return { icon: 'chatbubbles', title: 'addon.messages.messages', page: 'AddonMessagesIndexPage', - class: 'addon-messages-handler' + 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. + /* + $mmaPushNotifications = $mmAddonManager.get('$mmaPushNotifications'); + if ($mmaPushNotifications) { + $mmaPushNotifications.updateAddonCounter(siteId, 'mmaMessages', unread); + }*/ + }).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()) { + /*$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.ts b/src/addon/messages/providers/messages.ts index eb657a0f3..1f7076827 100644 --- a/src/addon/messages/providers/messages.ts +++ b/src/addon/messages/providers/messages.ts @@ -140,9 +140,9 @@ export class AddonMessagesProvider { 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'] = 0; - preSets['saveToCache'] = 0; - preSets['emergencyCache'] = 0; + preSets['getFromCache'] = false; + preSets['saveToCache'] = false; + preSets['emergencyCache'] = false; } // Get message received by current user. @@ -385,6 +385,94 @@ export class AddonMessagesProvider { }); } + /** + * 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 = { + getFromCache: false, + emergencyCache: false, + saveToCache: false, + 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 = {}; + let count; + response.messages.forEach((message) => { + discussions[message.useridto] = 1; + }); + 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 contacts cache. * @@ -436,6 +524,16 @@ export class AddonMessagesProvider { return this.sitesProvider.getCurrentSite().wsAvailable('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.getCurrentSite().wsAvailable('core_message_get_unread_conversations_count'); + } + /** * Returns whether or not the plugin is enabled in a certain site. * 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/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..94cd7cccb 100644 --- a/src/core/mainmenu/pages/menu/menu.ts +++ b/src/core/mainmenu/pages/menu/menu.ts @@ -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,16 @@ 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; + tab.loading = false; + } + }, site.getId()); + this.subscription = this.menuDelegate.getHandlers().subscribe((handlers) => { handlers = handlers.slice(0, CoreMainMenuProvider.NUM_MAIN_HANDLERS); // Get main handlers. @@ -128,5 +139,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 7bbd6247f..63c01f41f 100644 --- a/src/core/mainmenu/pages/more/more.html +++ b/src/core/mainmenu/pages/more/more.html @@ -18,9 +18,8 @@

{{ handler.title | translate}}

- - + {{badge}} +
diff --git a/src/core/mainmenu/pages/more/more.ts b/src/core/mainmenu/pages/more/more.ts index 5c5aa582e..be392a961 100644 --- a/src/core/mainmenu/pages/more/more.ts +++ b/src/core/mainmenu/pages/more/more.ts @@ -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,16 @@ 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; + handler.loading = false; + } + }, this.sitesProvider.getCurrentSiteId()); } /** @@ -68,6 +80,7 @@ export class CoreMainMenuMorePage implements OnDestroy { if (this.subscription) { this.subscription.unsubscribe(); } + this.updateBadgeObserver && this.updateBadgeObserver.off(); } /** diff --git a/src/core/mainmenu/providers/delegate.ts b/src/core/mainmenu/providers/delegate.ts index 191cafbdf..de7302f8b 100644 --- a/src/core/mainmenu/providers/delegate.ts +++ b/src/core/mainmenu/providers/delegate.ts @@ -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; });