MOBILE-3881 menu: Add badge on more tab

main
Pau Ferrer Ocaña 2021-11-03 15:56:22 +01:00
parent f765976247
commit 375301b788
8 changed files with 137 additions and 51 deletions

View File

@ -23,11 +23,11 @@ import { CoreSites } from '@services/sites';
import { CoreEvents } from '@singletons/events'; import { CoreEvents } from '@singletons/events';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { import {
CorePushNotifications,
CorePushNotificationsNotificationBasicData, CorePushNotificationsNotificationBasicData,
} from '@features/pushnotifications/services/pushnotifications'; } from '@features/pushnotifications/services/pushnotifications';
import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate'; import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { CoreMainMenuProvider } from '@features/mainmenu/services/mainmenu';
/** /**
* Handler to inject an option into main menu. * Handler to inject an option into main menu.
@ -90,7 +90,7 @@ export class AddonMessagesMainMenuHandlerService implements CoreMainMenuHandler,
); );
// Register Badge counter. // Register Badge counter.
CorePushNotificationsDelegate.registerCounterHandler('AddonMessages'); CorePushNotificationsDelegate.registerCounterHandler(AddonMessagesMainMenuHandlerService.name);
} }
/** /**
@ -162,7 +162,14 @@ export class AddonMessagesMainMenuHandlerService implements CoreMainMenuHandler,
} }
// Update push notifications badge. // Update push notifications badge.
CorePushNotifications.updateAddonCounter('AddonMessages', totalCount, siteId); CoreEvents.trigger(
CoreMainMenuProvider.MAIN_MENU_HANDLER_BADGE_UPDATED,
{
handler: AddonMessagesMainMenuHandlerService.name,
value: totalCount,
},
siteId,
);
} }
/** /**

View File

@ -22,6 +22,7 @@ import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@features/mainmenu
import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate'; import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate';
import { AddonNotifications, AddonNotificationsProvider } from '../notifications'; import { AddonNotifications, AddonNotificationsProvider } from '../notifications';
import { CoreMainMenuProvider } from '@features/mainmenu/services/mainmenu';
/** /**
* Handler to inject an option into main menu. * Handler to inject an option into main menu.
@ -72,7 +73,7 @@ export class AddonNotificationsMainMenuHandlerService implements CoreMainMenuHan
}); });
// Register Badge counter. // Register Badge counter.
CorePushNotificationsDelegate.registerCounterHandler('AddonNotifications'); CorePushNotificationsDelegate.registerCounterHandler(AddonNotificationsMainMenuHandlerService.name);
} }
/** /**
@ -112,10 +113,20 @@ export class AddonNotificationsMainMenuHandlerService implements CoreMainMenuHan
try { try {
const unreadCountData = await AddonNotifications.getUnreadNotificationsCount(undefined, siteId); const unreadCountData = await AddonNotifications.getUnreadNotificationsCount(undefined, siteId);
this.handlerData.badge = unreadCountData.count > 0 ? this.handlerData.badge = unreadCountData.count > 0
unreadCountData.count + (unreadCountData.hasMore ? '+' : '') : ? unreadCountData.count + (unreadCountData.hasMore ? '+' : '')
''; : '';
CorePushNotifications.updateAddonCounter('AddonNotifications', unreadCountData.count, siteId);
CorePushNotifications.updateAddonCounter(AddonNotificationsMainMenuHandlerService.name, unreadCountData.count, siteId);
CoreEvents.trigger(
CoreMainMenuProvider.MAIN_MENU_HANDLER_BADGE_UPDATED,
{
handler: AddonNotificationsMainMenuHandlerService.name,
value: unreadCountData.count,
},
siteId,
);
} catch { } catch {
this.handlerData.badge = ''; this.handlerData.badge = '';
} finally { } finally {

View File

@ -9,9 +9,9 @@
<ion-tab-button *ngFor="let tab of tabs" (keydown)="tabAction.keyDown($event)" (keyup)="tabAction.keyUp(tab.page, $event)" <ion-tab-button *ngFor="let tab of tabs" (keydown)="tabAction.keyDown($event)" (keyup)="tabAction.keyUp(tab.page, $event)"
[hidden]="!loaded && tab.hide" [tab]="tab.page" [disabled]="tab.hide" layout="label-hide" class="{{tab.class}}" [hidden]="!loaded && tab.hide" [tab]="tab.page" [disabled]="tab.hide" layout="label-hide" class="{{tab.class}}"
[selected]="tab.page === selectedTab" [tabindex]="selectedTab == tab.page ? 0 : -1" [attr.aria-controls]="tab.id"> [selected]="tab.page === selectedTab" [tabindex]="selectedTab == tab.page ? 0 : -1" [attr.aria-controls]="tab.id">
<ion-icon [name]="tab.icon" aria-hidden="true"></ion-icon> <ion-icon class="core-tab-icon" [name]="tab.icon" aria-hidden="true"></ion-icon>
<ion-label aria-hidden="true">{{ tab.title | translate }}</ion-label> <ion-label aria-hidden="true">{{ tab.title | translate }}</ion-label>
<ion-badge *ngIf="tab.badge" aria-hidden="true">{{ tab.badge }}</ion-badge> <ion-badge class="core-tab-badge" *ngIf="tab.badge" aria-hidden="true">{{ tab.badge }}</ion-badge>
<span class="sr-only">{{ tab.title | translate }}</span> <span class="sr-only">{{ tab.title | translate }}</span>
<span *ngIf="tab.badge && tab.badgeA11yText" class="sr-only"> <span *ngIf="tab.badge && tab.badgeA11yText" class="sr-only">
{{ tab.badgeA11yText | translate: {$a : tab.badge } }} {{ tab.badgeA11yText | translate: {$a : tab.badge } }}
@ -20,9 +20,10 @@
<ion-tab-button (keydown)="tabAction.keyDown($event)" (keyup)="tabAction.keyUp(morePageName, $event)" [hidden]="!loaded" <ion-tab-button (keydown)="tabAction.keyDown($event)" (keyup)="tabAction.keyUp(morePageName, $event)" [hidden]="!loaded"
[tab]="morePageName" layout="label-hide" [tabindex]="selectedTab == morePageName ? 0 : -1" [attr.aria-controls]="morePageName"> [tab]="morePageName" layout="label-hide" [tabindex]="selectedTab == morePageName ? 0 : -1" [attr.aria-controls]="morePageName">
<ion-icon name="ellipsis-horizontal" aria-hidden="true"></ion-icon> <ion-icon class="core-tab-icon" name="ellipsis-horizontal" aria-hidden="true"></ion-icon>
<ion-label aria-hidden="true">{{ 'core.more' | translate }}</ion-label> <ion-label aria-hidden="true">{{ 'core.more' | translate }}</ion-label>
<span class="sr-only">{{ 'core.more' | translate }}</span> <span class="sr-only">{{ 'core.more' | translate }}</span>
<ion-icon *ngIf="moreBadge" class="core-tab-badge" name="fas-circle" aria-hidden="true"></ion-icon>
</ion-tab-button> </ion-tab-button>
</ion-tab-bar> </ion-tab-bar>
</ion-tabs> </ion-tabs>

View File

@ -25,17 +25,17 @@
} }
ion-tab-button ion-icon { ion-tab-button ion-icon.core-tab-icon {
text-overflow: unset; text-overflow: unset;
overflow: visible; overflow: visible;
text-align: center; text-align: center;
} }
ion-tab-button.ios ion-icon { ion-tab-button.ios ion-icon.core-tab-icon {
font-size: 25px; font-size: 25px;
} }
ion-tab-button.md ion-badge { ion-tab-button.md ion-badge.core-tab-badge {
font-size: 12px; font-size: 12px;
font-weight: bold; font-weight: bold;
border-radius: 10px; border-radius: 10px;
@ -48,11 +48,29 @@
background: var(--background-selected); background: var(--background-selected);
} }
ion-icon.core-tab-badge {
color: var(--core-bottom-tabs-badge-color);
padding: 3px 6px 2px;
@include position(8px, null, null, calc(50% + 6px));
min-width: 12px;
font-size: 8px;
font-weight: normal;
box-sizing: border-box;
position: absolute;
z-index: 1;
}
ion-badge.core-tab-badge {
--background: var(--core-bottom-tabs-badge-color);
--color: var(--core-bottom-tabs-badge-text-color);
}
ion-tabs.placement-bottom ion-tab-button { ion-tabs.placement-bottom ion-tab-button {
ion-icon { ion-icon.core-tab-icon {
transition: margin 500ms ease-in-out, transform 300ms ease-in-out; transition: margin 500ms ease-in-out, transform 300ms ease-in-out;
} }
ion-badge { ion-icon.core-tab-badge,
ion-badge.core-tab-badge {
top: 8px; top: 8px;
} }
} }
@ -76,7 +94,8 @@
min-height: var(--menutabbar-size); min-height: var(--menutabbar-size);
flex: 0; flex: 0;
ion-badge { ion-icon.core-tab-badge,
ion-badge.core-tab-badge {
top: calc(50% - 20px); top: calc(50% - 20px);
} }
} }
@ -114,11 +133,11 @@
:host-context(.core-online), :host-context(.core-online),
:host-context(.core-offline) { :host-context(.core-offline) {
ion-tabs.placement-bottom ion-tab-button ion-icon { ion-tabs.placement-bottom ion-tab-button ion-icon.core-tab-icon {
margin-bottom: 8px; margin-bottom: 8px;
} }
ion-tabs.placement-bottom ion-tab-button.ios ion-icon { ion-tabs.placement-bottom ion-tab-button.ios ion-icon.core-tab-icon {
margin-bottom: 14px; margin-bottom: 14px;
} }

View File

@ -28,6 +28,7 @@ import { CoreNavigator } from '@services/navigator';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
import { NavigationEnd } from '@angular/router'; import { NavigationEnd } from '@angular/router';
import { trigger, state, style, transition, animate } from '@angular/animations'; import { trigger, state, style, transition, animate } from '@angular/animations';
import { CoreSites } from '@services/sites';
/** /**
* Page that displays the main menu of the app. * Page that displays the main menu of the app.
@ -66,10 +67,12 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
morePageName = CoreMainMenuProvider.MORE_PAGE_NAME; morePageName = CoreMainMenuProvider.MORE_PAGE_NAME;
selectedTab?: string; selectedTab?: string;
isMainScreen = false; isMainScreen = false;
moreBadge = false;
protected subscription?: Subscription; protected subscription?: Subscription;
protected navSubscription?: Subscription; protected navSubscription?: Subscription;
protected keyboardObserver?: CoreEventObserver; protected keyboardObserver?: CoreEventObserver;
protected badgeUpdateObserver?: CoreEventObserver;
protected resizeFunction: () => void; protected resizeFunction: () => void;
protected backButtonFunction: (event: BackButtonEvent) => void; protected backButtonFunction: (event: BackButtonEvent) => void;
protected selectHistory: string[] = []; protected selectHistory: string[] = [];
@ -102,11 +105,17 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
this.subscription = CoreMainMenuDelegate.getHandlersObservable().subscribe((handlers) => { this.subscription = CoreMainMenuDelegate.getHandlersObservable().subscribe((handlers) => {
// Remove the handlers that should only appear in the More menu. // Remove the handlers that should only appear in the More menu.
this.allHandlers = handlers.filter((handler) => !handler.onlyInMore); this.allHandlers = handlers;
this.initHandlers(); this.initHandlers();
}); });
this.badgeUpdateObserver = CoreEvents.on(CoreMainMenuProvider.MAIN_MENU_HANDLER_BADGE_UPDATED, (data) => {
if (data.siteId == CoreSites.getCurrentSiteId()) {
this.updateMoreBadge();
}
});
window.addEventListener('resize', this.resizeFunction); window.addEventListener('resize', this.resizeFunction);
document.addEventListener('ionBackButton', this.backButtonFunction); document.addEventListener('ionBackButton', this.backButtonFunction);
@ -130,10 +139,14 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
* Init handlers on change (size or handlers). * Init handlers on change (size or handlers).
*/ */
initHandlers(): void { initHandlers(): void {
if (this.allHandlers) { if (!this.allHandlers) {
return;
}
this.tabsPlacement = CoreMainMenu.getTabPlacement(); this.tabsPlacement = CoreMainMenu.getTabPlacement();
const handlers = this.allHandlers.slice(0, CoreMainMenu.getNumItems()); // Get main handlers. const handlers = this.allHandlers
.filter((handler) => !handler.onlyInMore)
.slice(0, CoreMainMenu.getNumItems()); // Get main handlers.
// Re-build the list of tabs. If a handler is already in the list, use existing object to prevent re-creating the tab. // Re-build the list of tabs. If a handler is already in the list, use existing object to prevent re-creating the tab.
const newTabs: CoreMainMenuHandlerToDisplay[] = []; const newTabs: CoreMainMenuHandlerToDisplay[] = [];
@ -156,8 +169,22 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
// Sort them by priority so new handlers are in the right position. // Sort them by priority so new handlers are in the right position.
this.tabs.sort((a, b) => (b.priority || 0) - (a.priority || 0)); this.tabs.sort((a, b) => (b.priority || 0) - (a.priority || 0));
this.updateMoreBadge();
this.loaded = CoreMainMenuDelegate.areHandlersLoaded(); this.loaded = CoreMainMenuDelegate.areHandlersLoaded();
} }
/**
* Check all non visible tab handlers for any badge text or number.
*/
updateMoreBadge(): void {
if (!this.allHandlers) {
return;
}
const numItems = CoreMainMenu.getNumItems();
this.moreBadge = this.allHandlers.some((handler, index) => (handler.onlyInMore || index >= numItems) && !!handler.badge);
} }
/** /**
@ -169,6 +196,7 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
window.removeEventListener('resize', this.resizeFunction); window.removeEventListener('resize', this.resizeFunction);
document.removeEventListener('ionBackButton', this.backButtonFunction); document.removeEventListener('ionBackButton', this.backButtonFunction);
this.keyboardObserver?.off(); this.keyboardObserver?.off();
this.badgeUpdateObserver?.off();
} }
/** /**

View File

@ -23,6 +23,19 @@ import { Device, makeSingleton } from '@singletons';
import { CoreArray } from '@singletons/array'; import { CoreArray } from '@singletons/array';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
declare module '@singletons/events' {
/**
* Augment CoreEventsData interface with events specific to this service.
*
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
*/
export interface CoreEventsData {
[CoreMainMenuProvider.MAIN_MENU_HANDLER_BADGE_UPDATED]: CoreMainMenuHandlerBadgeUpdatedEventData;
}
}
/** /**
* Service that provides some features regarding Main Menu. * Service that provides some features regarding Main Menu.
*/ */
@ -32,6 +45,7 @@ export class CoreMainMenuProvider {
static readonly NUM_MAIN_HANDLERS = 4; static readonly NUM_MAIN_HANDLERS = 4;
static readonly ITEM_MIN_WIDTH = 72; // Min with of every item, based on 5 items on a 360 pixel wide screen. static readonly ITEM_MIN_WIDTH = 72; // Min with of every item, based on 5 items on a 360 pixel wide screen.
static readonly MORE_PAGE_NAME = 'more'; static readonly MORE_PAGE_NAME = 'more';
static readonly MAIN_MENU_HANDLER_BADGE_UPDATED = 'main_menu_handler_badge_updated';
protected tablet = false; protected tablet = false;
@ -339,3 +353,8 @@ type CustomMenuItemsMap = Record<string, {
}; };
}; };
}>; }>;
export type CoreMainMenuHandlerBadgeUpdatedEventData = {
handler: string; // Handler name.
value: number; // New counter value.
};

View File

@ -41,6 +41,7 @@ import {
import { CoreError } from '@classes/errors/error'; import { CoreError } from '@classes/errors/error';
import { CoreWSExternalWarning } from '@services/ws'; import { CoreWSExternalWarning } from '@services/ws';
import { CoreSitesFactory } from '@services/sites-factory'; import { CoreSitesFactory } from '@services/sites-factory';
import { CoreMainMenuProvider } from '@features/mainmenu/services/mainmenu';
/** /**
* Service to handle push notifications. * Service to handle push notifications.
@ -101,6 +102,10 @@ export class CorePushNotificationsProvider {
} }
}); });
CoreEvents.on(CoreMainMenuProvider.MAIN_MENU_HANDLER_BADGE_UPDATED, (data) => {
this.updateAddonCounter(data.handler, data.value, data.siteId);
});
// Listen for local notification clicks (generated by the app). // Listen for local notification clicks (generated by the app).
CoreLocalNotifications.registerClick<CorePushNotificationsNotificationBasicData>( CoreLocalNotifications.registerClick<CorePushNotificationsNotificationBasicData>(
CorePushNotificationsProvider.COMPONENT, CorePushNotificationsProvider.COMPONENT,

View File

@ -97,10 +97,6 @@
--color: var(--core-bottom-tabs-color); --color: var(--core-bottom-tabs-color);
--color-selected: var(--core-bottom-tabs-color-selected); --color-selected: var(--core-bottom-tabs-color-selected);
--background-selected: var(--core-bottom-tabs-background-selected); --background-selected: var(--core-bottom-tabs-background-selected);
ion-badge {
--background: var(--core-bottom-tabs-badge-color);
--color: var(--core-bottom-tabs-badge-text-color);
}
} }
--core-link-color: var(--blue); --core-link-color: var(--blue);