MOBILE-3881 menu: Add badge on more tab
parent
f765976247
commit
375301b788
|
@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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.
|
||||||
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue