MOBILE-2327 messages: Add badge to main menu

main
Pau Ferrer Ocaña 2018-02-22 15:21:22 +01:00
parent 7c42b886b1
commit 7790596edf
9 changed files with 307 additions and 18 deletions

View File

@ -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(() => {

View File

@ -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<any>} Promise resolved when done, rejected if failure.
*/
execute(siteId?: string): Promise<any> {
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<any>} Promise resolved with the notifications.
*/
protected fetchMessages(siteId?: string): Promise<any> {
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<any>} Promise resolved with an object with title and text.
*/
protected getTitleAndText(message: any): Promise<any> {
const data = {
title: message.userfromfullname,
};
return this.textUtils.formatText(message.text, true, true).catch(() => {
return message.text;
}).then((formattedText) => {
data['text'] = formattedText;
return data;
});
}
}

View File

@ -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<any>} Promise resolved with the message unread count.
*/
getUnreadConversationsCount(userId?: number, siteId?: string): Promise<any> {
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<any>} Promise resolved with the message unread count.
*/
getUnreadReceivedMessages(toDisplay: boolean = true, forceCache: boolean = false, ignoreCache: boolean = false,
siteId?: string): Promise<any> {
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.
*

View File

@ -43,7 +43,7 @@ core-course-module {
pointer-events: auto;
}
.core-module-buttons-more .spinner {
.core-module-buttons-more .spinner {
right: 13px;
position: absolute;
}

View File

@ -1,4 +1,4 @@
<ion-tabs *ngIf="loaded" #mainTabs [selectedIndex]="initialTab" tabsPlacement="bottom" tabsLayout="title-hide">
<ion-tab [enabled]="false" [show]="false" [root]="redirectPage" [rootParams]="redirectParams"></ion-tab>
<ion-tab *ngFor="let tab of tabs" [root]="tab.page" [tabTitle]="tab.title | translate" [tabIcon]="tab.icon" class="{{tab.class}}"></ion-tab>
<ion-tab *ngFor="let tab of tabs" [root]="tab.page" [tabTitle]="tab.title | translate" [tabIcon]="tab.icon" [tabBadge]="tab.badge" class="{{tab.class}}"></ion-tab>
</ion-tabs>

View File

@ -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();
}
}

View File

@ -18,9 +18,8 @@
<ion-item *ngFor="let handler of handlers" [ngClass]="['core-moremenu-handler', handler.class]" (click)="openHandler(handler)" title="{{ handler.title | translate }}" detail-push>
<ion-icon [name]="handler.icon" item-start></ion-icon>
<p>{{ handler.title | translate}}</p>
<!-- @todo: Badge. -->
<!-- <span ng-show="!loading && badge" class="badge badge-positive">{{badge}}</span>
<ion-spinner ng-if="loading" class="icon"></ion-spinner> -->
<ion-badge item-end *ngIf="handler.showBadge" [hidden]="handler.loading || !handler.badge">{{badge}}</ion-badge>
<ion-spinner item-end *ngIf="handler.showBadge && handler.loading"></ion-spinner>
</ion-item>
<div *ngFor="let item of customItems" class="core-moremenu-customitem">
<a ion-item *ngIf="item.type != 'embedded'" [href]="item.url" core-link [capture]="item.type == 'app'" [inApp]="item.type == 'inappbrowser'" title="{{item.label}}">

View File

@ -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();
}
/**

View File

@ -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<CoreMainMenuHandlerToDisplay[]> = new BehaviorSubject<CoreMainMenuHandlerToDisplay[]>([]);
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;
});