diff --git a/src/addons/block/onlineusers/components/onlineusers/onlineusers.scss b/src/addons/block/onlineusers/components/onlineusers/onlineusers.scss index c3d4b4c40..c34c11af4 100644 --- a/src/addons/block/onlineusers/components/onlineusers/onlineusers.scss +++ b/src/addons/block/onlineusers/components/onlineusers/onlineusers.scss @@ -28,7 +28,7 @@ } .userpicture { - vertical-align: text-bottom; + border-radius: 50%; } } diff --git a/src/addons/block/timeline/components/components.module.ts b/src/addons/block/timeline/components/components.module.ts index dfd4a5fc4..f54298186 100644 --- a/src/addons/block/timeline/components/components.module.ts +++ b/src/addons/block/timeline/components/components.module.ts @@ -15,8 +15,6 @@ import { NgModule } from '@angular/core'; import { CoreSharedModule } from '@/core/shared.module'; -import { CoreCoursesComponentsModule } from '@features/courses/components/components.module'; -import { CoreCourseComponentsModule } from '@features/course/components/components.module'; import { AddonBlockTimelineComponent } from './timeline/timeline'; import { AddonBlockTimelineEventsComponent } from './events/events'; @@ -28,8 +26,6 @@ import { AddonBlockTimelineEventsComponent } from './events/events'; ], imports: [ CoreSharedModule, - CoreCoursesComponentsModule, - CoreCourseComponentsModule, ], exports: [ AddonBlockTimelineComponent, diff --git a/src/addons/block/timeline/components/events/addon-block-timeline-events.html b/src/addons/block/timeline/components/events/addon-block-timeline-events.html index 0622e2e0b..7907f508c 100644 --- a/src/addons/block/timeline/components/events/addon-block-timeline-events.html +++ b/src/addons/block/timeline/components/events/addon-block-timeline-events.html @@ -1,50 +1,61 @@ + + +

+ {{ 'core.courses.aria:coursename' | translate }} + +

+
+
- -

{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedayshort" }}

-
+ + +

{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedayshort" }}

+
+
- - - + -

- - -

-

- - -

- - - {{event.action.name}} - {{event.action.itemcount}} - - + + + + + {{event.timesort * 1000 | coreFormatDate:"strftimetime24" }} + + + + +

+ + + {{ 'addon.block_timeline.overdue' | translate }} + +

+

+ + + · + + + +

+
+
+
+ + + {{event.action.name}} + + {{event.action.itemcount}} + + + +
- -
-
- {{event.timesort * 1000 | coreFormatDate:"strftimetime24" }} -
- - {{event.action.name}} - - {{event.action.itemcount}} - - -
@@ -57,6 +68,10 @@ - - + + +

{{'addon.block_timeline.noevents' | translate}}

+
+
+ diff --git a/src/addons/block/timeline/components/events/events.scss b/src/addons/block/timeline/components/events/events.scss index c863dbb2a..8ea49fcb1 100644 --- a/src/addons/block/timeline/components/events/events.scss +++ b/src/addons/block/timeline/components/events/events.scss @@ -1,6 +1,41 @@ -.events-info { - display: flex; - flex-direction: column; - text-align: end; - padding: 10px 0; +@import "~theme/globals"; + +h3 { + font-weight: bold; + font-size: 18px; +} + +h4 { + font-size: 15px; +} + +h4.core-bold { + font-weight: bold; +} + +.addon-block-timeline-activity ion-badge { + @include margin-horizontal(0.25rem, 0.5rem); +} + +.addon-block-timeline-activity core-mod-icon { + --margin-end: 0.5rem; +} + +.addon-block-timeline-activity-time, +.addon-block-timeline-activity-action { + flex-grow: 0; +} + +.addon-block-timeline-activity-main, +.addon-block-timeline-activity-name { + flex-grow: 1; + p { + overflow: hidden; + text-overflow: ellipsis; + } +} + +.addon-block-timeline-activity-name { + flex-grow: 1; + overflow: hidden; } diff --git a/src/addons/block/timeline/components/events/events.ts b/src/addons/block/timeline/components/events/events.ts index b0db9c8a0..b9047ff2b 100644 --- a/src/addons/block/timeline/components/events/events.ts +++ b/src/addons/block/timeline/components/events/events.ts @@ -17,11 +17,11 @@ import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; -import { CoreUtils } from '@services/utils/utils'; import { CoreCourse } from '@features/course/services/course'; import moment from 'moment'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; import { AddonCalendarEvent } from '@addons/calendar/services/calendar'; +import { CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper'; /** * Directive to render a list of events in course overview. @@ -34,34 +34,37 @@ import { AddonCalendarEvent } from '@addons/calendar/services/calendar'; export class AddonBlockTimelineEventsComponent implements OnChanges { @Input() events: AddonBlockTimelineEvent[] = []; // The events to render. - @Input() showCourse?: boolean | string; // Whether to show the course name. + @Input() course?: CoreEnrolledCourseDataWithOptions; // Whether to show the course name. @Input() from = 0; // Number of days from today to offset the events. @Input() to?: number; // Number of days from today to limit the events to. If not defined, no limit. - @Input() canLoadMore?: boolean; // Whether more events can be loaded. - @Output() loadMore: EventEmitter; // Notify that more events should be loaded. + @Input() canLoadMore = false; // Whether more events can be loaded. + @Output() loadMore = new EventEmitter(); // Notify that more events should be loaded. + showCourse = false; // Whether to show the course name. empty = true; loadingMore = false; filteredEvents: AddonBlockTimelineEventFilteredEvent[] = []; - constructor() { - this.loadMore = new EventEmitter(); - } - /** - * Detect changes on input properties. + * @inheritdoc */ async ngOnChanges(changes: {[name: string]: SimpleChange}): Promise { - this.showCourse = CoreUtils.isTrueOrOne(this.showCourse); + this.showCourse = !this.course; if (changes.events || changes.from || changes.to) { if (this.events && this.events.length > 0) { const filteredEvents = await this.filterEventsByTime(this.from, this.to); this.empty = !filteredEvents || filteredEvents.length <= 0; + const now = CoreTimeUtils.timestamp(); + const eventsByDay: Record = {}; filteredEvents.forEach((event) => { const dayTimestamp = CoreTimeUtils.getMidnightForTimestamp(event.timesort); + + // Already calculated on 4.0 onwards but this will be live. + event.overdue = event.timesort < now; + if (eventsByDay[dayTimestamp]) { eventsByDay[dayTimestamp].push(event); } else { @@ -69,15 +72,13 @@ export class AddonBlockTimelineEventsComponent implements OnChanges { } }); - const todaysMidnight = CoreTimeUtils.getMidnightForTimestamp(); - this.filteredEvents = []; - Object.keys(eventsByDay).forEach((key) => { + this.filteredEvents = Object.keys(eventsByDay).map((key) => { const dayTimestamp = parseInt(key); - this.filteredEvents.push({ - color: dayTimestamp < todaysMidnight ? 'danger' : 'light', + + return { dayTimestamp, events: eventsByDay[dayTimestamp], - }); + }; }); } else { this.empty = true; @@ -94,7 +95,7 @@ export class AddonBlockTimelineEventsComponent implements OnChanges { */ protected async filterEventsByTime(start: number, end?: number): Promise { start = moment().add(start, 'days').startOf('day').unix(); - end = typeof end != 'undefined' ? moment().add(end, 'days').startOf('day').unix() : end; + end = end !== undefined ? moment().add(end, 'days').startOf('day').unix() : end; return await Promise.all(this.events.filter((event) => { if (end) { @@ -122,12 +123,12 @@ export class AddonBlockTimelineEventsComponent implements OnChanges { /** * Action clicked. * - * @param e Click event. + * @param event Click event. * @param url Url of the action. */ - async action(e: Event, url: string): Promise { - e.preventDefault(); - e.stopPropagation(); + async action(event: Event, url: string): Promise { + event.preventDefault(); + event.stopPropagation(); // Fix URL format. url = CoreTextUtils.decodeHTMLEntities(url); @@ -137,7 +138,7 @@ export class AddonBlockTimelineEventsComponent implements OnChanges { try { const treated = await CoreContentLinksHelper.handleLink(url); if (!treated) { - return CoreSites.getCurrentSite()?.openInBrowserWithAutoLoginIfSameSite(url); + return CoreSites.getRequiredCurrentSite().openInBrowserWithAutoLoginIfSameSite(url); } } finally { modal.dismiss(); @@ -154,5 +155,4 @@ type AddonBlockTimelineEvent = AddonCalendarEvent & { type AddonBlockTimelineEventFilteredEvent = { events: AddonBlockTimelineEvent[]; dayTimestamp: number; - color: string; }; diff --git a/src/addons/block/timeline/components/timeline/addon-block-timeline.html b/src/addons/block/timeline/components/timeline/addon-block-timeline.html index 62105627c..81c32360d 100644 --- a/src/addons/block/timeline/components/timeline/addon-block-timeline.html +++ b/src/addons/block/timeline/components/timeline/addon-block-timeline.html @@ -1,5 +1,7 @@ -

{{ 'addon.block_timeline.pluginname' | translate }}

+ +

{{ 'addon.block_timeline.pluginname' | translate }}

+
@@ -18,7 +20,7 @@ {{ 'addon.block_timeline.overdue' | translate }} - + {{ 'addon.block_timeline.duedate' | translate }} @@ -36,21 +38,14 @@ - + - - - - - - - - - - + + + + diff --git a/src/addons/block/timeline/components/timeline/timeline.ts b/src/addons/block/timeline/components/timeline/timeline.ts index 29f2e1487..c8ef64b77 100644 --- a/src/addons/block/timeline/components/timeline/timeline.ts +++ b/src/addons/block/timeline/components/timeline/timeline.ts @@ -24,6 +24,7 @@ import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/ import { CoreSite } from '@classes/site'; import { CoreCourses } from '@features/courses/services/courses'; import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; +import { CoreNavigator } from '@services/navigator'; /** * Component to render a timeline block. @@ -36,7 +37,7 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen sort = 'sortbydates'; filter = 'next30days'; - currentSite?: CoreSite; + currentSite!: CoreSite; timeline: { events: AddonCalendarEvent[]; loaded: boolean; @@ -66,10 +67,18 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen } /** - * Component being initialized. + * @inheritdoc */ async ngOnInit(): Promise { - this.currentSite = CoreSites.getRequiredCurrentSite(); + try { + this.currentSite = CoreSites.getRequiredCurrentSite(); + } catch (error) { + CoreDomUtils.showErrorModal(error); + + CoreNavigator.back(); + + return; + } this.filter = await this.currentSite.getLocalSiteConfig('AddonBlockTimelineFilter', this.filter); this.switchFilter(this.filter); @@ -117,28 +126,21 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen } } - /** - * Load more events. - */ - async loadMoreTimeline(): Promise { - try { - await this.fetchMyOverviewTimeline(this.timeline.canLoadMore); - } catch (error) { - CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError); - } - } - /** * Load more events. * - * @param course Course. + * @param course Course. If defined, it will update the course events, timeline otherwise. * @return Promise resolved when done. */ - async loadMoreCourse(course: AddonBlockTimelineCourse): Promise { + async loadMore(course?: AddonBlockTimelineCourse): Promise { try { - const courseEvents = await AddonBlockTimeline.getActionEventsByCourse(course.id, course.canLoadMore); - course.events = course.events?.concat(courseEvents.events); - course.canLoadMore = courseEvents.canLoadMore; + if (course) { + const courseEvents = await AddonBlockTimeline.getActionEventsByCourse(course.id, course.canLoadMore); + course.events = course.events?.concat(courseEvents.events); + course.canLoadMore = courseEvents.canLoadMore; + } else { + await this.fetchMyOverviewTimeline(this.timeline.canLoadMore); + } } catch (error) { CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError); } @@ -188,12 +190,12 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen */ switchFilter(filter: string): void { this.filter = filter; - this.currentSite?.setLocalSiteConfig('AddonBlockTimelineFilter', this.filter); + this.currentSite.setLocalSiteConfig('AddonBlockTimelineFilter', this.filter); switch (this.filter) { case 'overdue': this.dataFrom = -14; - this.dataTo = 0; + this.dataTo = 1; break; case 'next7days': this.dataFrom = 0; @@ -226,7 +228,7 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen */ switchSort(sort: string): void { this.sort = sort; - this.currentSite?.setLocalSiteConfig('AddonBlockTimelineSort', this.sort); + this.currentSite.setLocalSiteConfig('AddonBlockTimelineSort', this.sort); if (!this.timeline.loaded && this.sort == 'sortbydates') { this.fetchContent(); @@ -237,7 +239,7 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen } -type AddonBlockTimelineCourse = CoreEnrolledCourseDataWithOptions & { +export type AddonBlockTimelineCourse = CoreEnrolledCourseDataWithOptions & { events?: AddonCalendarEvent[]; canLoadMore?: number; }; diff --git a/src/addons/calendar/services/calendar.ts b/src/addons/calendar/services/calendar.ts index b4ab29361..f21f71869 100644 --- a/src/addons/calendar/services/calendar.ts +++ b/src/addons/calendar/services/calendar.ts @@ -1707,14 +1707,19 @@ export type AddonCalendarEventBase = { userid?: number; // Userid. repeatid?: number; // Repeatid. eventcount?: number; // Eventcount. + component?: string; // Component. modulename?: string; // Modulename. + activityname?: string; // Activityname. + activitystr?: string; // Activitystr. instance?: number; // Instance. eventtype: AddonCalendarEventType; // Eventtype. timestart: number; // Timestart. timeduration: number; // Timeduration. timesort: number; // Timesort. + timeusermidnight: number; // Timeusermidnight. visible: number; // Visible. timemodified: number; // Timemodified. + overdue?: boolean; // Overdue. icon: { key: string; // Key. component: string; // Component. diff --git a/src/addons/messages/services/handlers/mainmenu.ts b/src/addons/messages/services/handlers/mainmenu.ts index 7b19afaa5..564f844ba 100644 --- a/src/addons/messages/services/handlers/mainmenu.ts +++ b/src/addons/messages/services/handlers/mainmenu.ts @@ -23,11 +23,11 @@ import { CoreSites } from '@services/sites'; import { CoreEvents } from '@singletons/events'; import { CoreUtils } from '@services/utils/utils'; import { - CorePushNotifications, CorePushNotificationsNotificationBasicData, } from '@features/pushnotifications/services/pushnotifications'; import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate'; import { makeSingleton } from '@singletons'; +import { CoreMainMenuProvider } from '@features/mainmenu/services/mainmenu'; /** * Handler to inject an option into main menu. @@ -90,7 +90,7 @@ export class AddonMessagesMainMenuHandlerService implements CoreMainMenuHandler, ); // Register Badge counter. - CorePushNotificationsDelegate.registerCounterHandler('AddonMessages'); + CorePushNotificationsDelegate.registerCounterHandler(AddonMessagesMainMenuHandlerService.name); } /** @@ -162,7 +162,14 @@ export class AddonMessagesMainMenuHandlerService implements CoreMainMenuHandler, } // Update push notifications badge. - CorePushNotifications.updateAddonCounter('AddonMessages', totalCount, siteId); + CoreEvents.trigger( + CoreMainMenuProvider.MAIN_MENU_HANDLER_BADGE_UPDATED, + { + handler: AddonMessagesMainMenuHandlerService.name, + value: totalCount, + }, + siteId, + ); } /** diff --git a/src/addons/notifications/services/handlers/mainmenu.ts b/src/addons/notifications/services/handlers/mainmenu.ts index a19caaae9..bd4800729 100644 --- a/src/addons/notifications/services/handlers/mainmenu.ts +++ b/src/addons/notifications/services/handlers/mainmenu.ts @@ -22,6 +22,7 @@ import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@features/mainmenu import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate'; import { AddonNotifications, AddonNotificationsProvider } from '../notifications'; +import { CoreMainMenuProvider } from '@features/mainmenu/services/mainmenu'; /** * Handler to inject an option into main menu. @@ -72,7 +73,7 @@ export class AddonNotificationsMainMenuHandlerService implements CoreMainMenuHan }); // Register Badge counter. - CorePushNotificationsDelegate.registerCounterHandler('AddonNotifications'); + CorePushNotificationsDelegate.registerCounterHandler(AddonNotificationsMainMenuHandlerService.name); } /** @@ -112,10 +113,20 @@ export class AddonNotificationsMainMenuHandlerService implements CoreMainMenuHan try { const unreadCountData = await AddonNotifications.getUnreadNotificationsCount(undefined, siteId); - this.handlerData.badge = unreadCountData.count > 0 ? - unreadCountData.count + (unreadCountData.hasMore ? '+' : '') : - ''; - CorePushNotifications.updateAddonCounter('AddonNotifications', unreadCountData.count, siteId); + this.handlerData.badge = unreadCountData.count > 0 + ? unreadCountData.count + (unreadCountData.hasMore ? '+' : '') + : ''; + + CorePushNotifications.updateAddonCounter(AddonNotificationsMainMenuHandlerService.name, unreadCountData.count, siteId); + + CoreEvents.trigger( + CoreMainMenuProvider.MAIN_MENU_HANDLER_BADGE_UPDATED, + { + handler: AddonNotificationsMainMenuHandlerService.name, + value: unreadCountData.count, + }, + siteId, + ); } catch { this.handlerData.badge = ''; } finally { diff --git a/src/core/features/block/components/block/block.scss b/src/core/features/block/components/block/block.scss index c33c495a9..72e49c32f 100644 --- a/src/core/features/block/components/block/block.scss +++ b/src/core/features/block/components/block/block.scss @@ -7,4 +7,8 @@ ion-item-divider { min-height: var(--item-divider-min-height); } + + ::ng-deep core-loading { + --loading-inline-min-height: 44px; + } } diff --git a/src/core/features/courses/components/course-progress/core-courses-course-progress.html b/src/core/features/courses/components/course-progress/core-courses-course-progress.html index d36b8cb38..7adb8fcc3 100644 --- a/src/core/features/courses/components/course-progress/core-courses-course-progress.html +++ b/src/core/features/courses/components/course-progress/core-courses-course-progress.html @@ -1,14 +1,12 @@
- +
- +

| - + @@ -35,12 +32,8 @@

-
@@ -50,9 +43,8 @@ [attr.aria-label]="'core.loading' | translate"> - +
- + -
diff --git a/src/core/features/mainmenu/pages/menu/menu.html b/src/core/features/mainmenu/pages/menu/menu.html index 18a5bb380..b7a79fa7f 100644 --- a/src/core/features/mainmenu/pages/menu/menu.html +++ b/src/core/features/mainmenu/pages/menu/menu.html @@ -9,9 +9,9 @@ - + - + {{ tab.title | translate }} {{ tab.badgeA11yText | translate: {$a : tab.badge } }} @@ -20,9 +20,10 @@ - + {{ 'core.more' | translate }} + diff --git a/src/core/features/mainmenu/pages/menu/menu.scss b/src/core/features/mainmenu/pages/menu/menu.scss index b359d98f0..993e0cea6 100644 --- a/src/core/features/mainmenu/pages/menu/menu.scss +++ b/src/core/features/mainmenu/pages/menu/menu.scss @@ -25,17 +25,17 @@ } - ion-tab-button ion-icon { + ion-tab-button ion-icon.core-tab-icon { text-overflow: unset; overflow: visible; text-align: center; } - ion-tab-button.ios ion-icon { + ion-tab-button.ios ion-icon.core-tab-icon { font-size: 25px; } - ion-tab-button.md ion-badge { + ion-tab-button.md ion-badge.core-tab-badge { font-size: 12px; font-weight: bold; border-radius: 10px; @@ -48,11 +48,29 @@ 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-icon { + ion-icon.core-tab-icon { 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; } } @@ -76,7 +94,8 @@ min-height: var(--menutabbar-size); flex: 0; - ion-badge { + ion-icon.core-tab-badge, + ion-badge.core-tab-badge { top: calc(50% - 20px); } } @@ -114,11 +133,11 @@ :host-context(.core-online), :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; } - 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; } diff --git a/src/core/features/mainmenu/pages/menu/menu.ts b/src/core/features/mainmenu/pages/menu/menu.ts index 36d929f4d..63799d858 100644 --- a/src/core/features/mainmenu/pages/menu/menu.ts +++ b/src/core/features/mainmenu/pages/menu/menu.ts @@ -28,6 +28,7 @@ import { CoreNavigator } from '@services/navigator'; import { filter } from 'rxjs/operators'; import { NavigationEnd } from '@angular/router'; import { trigger, state, style, transition, animate } from '@angular/animations'; +import { CoreSites } from '@services/sites'; /** * Page that displays the main menu of the app. @@ -66,10 +67,12 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { morePageName = CoreMainMenuProvider.MORE_PAGE_NAME; selectedTab?: string; isMainScreen = false; + moreBadge = false; protected subscription?: Subscription; protected navSubscription?: Subscription; protected keyboardObserver?: CoreEventObserver; + protected badgeUpdateObserver?: CoreEventObserver; protected resizeFunction: () => void; protected backButtonFunction: (event: BackButtonEvent) => void; protected selectHistory: string[] = []; @@ -102,11 +105,17 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { this.subscription = CoreMainMenuDelegate.getHandlersObservable().subscribe((handlers) => { // Remove the handlers that should only appear in the More menu. - this.allHandlers = handlers.filter((handler) => !handler.onlyInMore); + this.allHandlers = handlers; 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); document.addEventListener('ionBackButton', this.backButtonFunction); @@ -130,34 +139,52 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { * Init handlers on change (size or handlers). */ initHandlers(): void { - if (this.allHandlers) { - this.tabsPlacement = CoreMainMenu.getTabPlacement(); - - const handlers = this.allHandlers.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. - const newTabs: CoreMainMenuHandlerToDisplay[] = []; - - for (let i = 0; i < handlers.length; i++) { - const handler = handlers[i]; - - // Check if the handler is already in the tabs list. If so, use it. - const tab = this.tabs.find((tab) => tab.page == handler.page); - - tab ? tab.hide = false : null; - handler.hide = false; - handler.id = handler.id || 'core-mainmenu-' + CoreUtils.getUniqueId('CoreMainMenuPage'); - - newTabs.push(tab || handler); - } - - this.tabs = newTabs; - - // Sort them by priority so new handlers are in the right position. - this.tabs.sort((a, b) => (b.priority || 0) - (a.priority || 0)); - - this.loaded = CoreMainMenuDelegate.areHandlersLoaded(); + if (!this.allHandlers) { + return; } + this.tabsPlacement = CoreMainMenu.getTabPlacement(); + + 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. + const newTabs: CoreMainMenuHandlerToDisplay[] = []; + + for (let i = 0; i < handlers.length; i++) { + const handler = handlers[i]; + + // Check if the handler is already in the tabs list. If so, use it. + const tab = this.tabs.find((tab) => tab.page == handler.page); + + tab ? tab.hide = false : null; + handler.hide = false; + handler.id = handler.id || 'core-mainmenu-' + CoreUtils.getUniqueId('CoreMainMenuPage'); + + newTabs.push(tab || handler); + } + + this.tabs = newTabs; + + // Sort them by priority so new handlers are in the right position. + this.tabs.sort((a, b) => (b.priority || 0) - (a.priority || 0)); + + this.updateMoreBadge(); + + 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); document.removeEventListener('ionBackButton', this.backButtonFunction); this.keyboardObserver?.off(); + this.badgeUpdateObserver?.off(); } /** diff --git a/src/core/features/mainmenu/services/mainmenu.ts b/src/core/features/mainmenu/services/mainmenu.ts index 683f8aea7..d34ecde37 100644 --- a/src/core/features/mainmenu/services/mainmenu.ts +++ b/src/core/features/mainmenu/services/mainmenu.ts @@ -23,6 +23,19 @@ import { Device, makeSingleton } from '@singletons'; import { CoreArray } from '@singletons/array'; 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. */ @@ -32,6 +45,7 @@ export class CoreMainMenuProvider { 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 MORE_PAGE_NAME = 'more'; + static readonly MAIN_MENU_HANDLER_BADGE_UPDATED = 'main_menu_handler_badge_updated'; protected tablet = false; @@ -339,3 +353,8 @@ type CustomMenuItemsMap = Record; + +export type CoreMainMenuHandlerBadgeUpdatedEventData = { + handler: string; // Handler name. + value: number; // New counter value. +}; diff --git a/src/core/features/pushnotifications/services/pushnotifications.ts b/src/core/features/pushnotifications/services/pushnotifications.ts index 6b02caf7e..9de4396d8 100644 --- a/src/core/features/pushnotifications/services/pushnotifications.ts +++ b/src/core/features/pushnotifications/services/pushnotifications.ts @@ -41,6 +41,7 @@ import { import { CoreError } from '@classes/errors/error'; import { CoreWSExternalWarning } from '@services/ws'; import { CoreSitesFactory } from '@services/sites-factory'; +import { CoreMainMenuProvider } from '@features/mainmenu/services/mainmenu'; /** * 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). CoreLocalNotifications.registerClick( CorePushNotificationsProvider.COMPONENT, diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index 2b12e3e8c..45f9de0ab 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -788,6 +788,13 @@ ion-select::part(icon) { opacity: 1; } +ion-select-popover ion-item.core-select-option-title { + cursor: pointer; + ion-radio { + display: none; + } +} + ion-searchbar { .searchbar-search-icon.ios { top: 4px; diff --git a/src/theme/theme.light.scss b/src/theme/theme.light.scss index fb0ad53ec..56998e1eb 100644 --- a/src/theme/theme.light.scss +++ b/src/theme/theme.light.scss @@ -97,10 +97,6 @@ --color: var(--core-bottom-tabs-color); --color-selected: var(--core-bottom-tabs-color-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);