commit
						798d916b1f
					
				| @ -28,7 +28,7 @@ | ||||
|                 } | ||||
| 
 | ||||
|                 .userpicture { | ||||
|                     vertical-align: text-bottom; | ||||
|                     border-radius: 50%; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -1,50 +1,61 @@ | ||||
| <ion-item lines="none" *ngIf="course"> | ||||
|     <ion-label class="ion-text-wrap"> | ||||
|         <h3> | ||||
|             <span class="sr-only">{{ 'core.courses.aria:coursename' | translate }}</span> | ||||
|             <core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id"></core-format-text> | ||||
|         </h3> | ||||
|     </ion-label> | ||||
| </ion-item> | ||||
| <ion-item-group *ngFor="let dayEvents of filteredEvents"> | ||||
|     <ion-item-divider [color]="dayEvents.color"> | ||||
|         <ion-label><h3>{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedayshort" }}</h3></ion-label> | ||||
|     </ion-item-divider> | ||||
|     <ion-item lines="none"> | ||||
|         <ion-label> | ||||
|             <h4 [class.core-bold]="!course">{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedayshort" }}</h4> | ||||
|         </ion-label> | ||||
|     </ion-item> | ||||
|     <ng-container *ngFor="let event of dayEvents.events"> | ||||
|         <ion-item class="ion-text-wrap core-course-module-handler item-media" detail="false" (click)="action($event, event.url)" | ||||
|             [attr.aria-label]="event.name" button> | ||||
|             <core-mod-icon *ngIf="event.iconUrl" slot="start" [modicon]="event.iconUrl" [componentId]="event.instance" | ||||
|                 [modname]="event.modulename"> | ||||
|             </core-mod-icon> | ||||
|         <ion-item class="addon-block-timeline-activity" detail="false" (click)="action($event, event.url)" [attr.aria-label]="event.name" | ||||
|             button lines="full"> | ||||
|             <ion-label> | ||||
|                 <p class="item-heading"> | ||||
|                     <core-format-text [text]="event.name" contextLevel="module" [contextInstanceId]="event.id" | ||||
|                         [courseId]="event.course && event.course.id"> | ||||
|                     </core-format-text> | ||||
|                 </p> | ||||
|                 <p *ngIf="showCourse && event.course"> | ||||
|                     <core-format-text [text]="event.course.fullnamedisplay" contextLevel="course" | ||||
|                         [contextInstanceId]="event.course.id"> | ||||
|                     </core-format-text> | ||||
|                 </p> | ||||
| 
 | ||||
|                 <ion-button fill="clear" class="ion-hide-md-up ion-text-wrap" (click)="action($event, event.action.url)" | ||||
|                     [title]="event.action.name" [disabled]="!event.action.actionable" *ngIf="event.action"> | ||||
|                     {{event.action.name}} | ||||
|                     <ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount">{{event.action.itemcount}} | ||||
|                     </ion-badge> | ||||
|                 </ion-button> | ||||
|                 <ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding"> | ||||
|                     <ion-col class="addon-block-timeline-activity-main ion-no-padding"> | ||||
|                         <ion-row class="ion-justify-content-between ion-align-items-center ion-nowrap ion-no-padding"> | ||||
|                             <ion-col class="addon-block-timeline-activity-time ion-no-padding"> | ||||
|                                 <ion-badge color="light">{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</ion-badge> | ||||
|                                 <core-mod-icon *ngIf="event.iconUrl" [modicon]="event.iconUrl" [componentId]="event.instance" | ||||
|                                     [modname]="event.modulename"> | ||||
|                                 </core-mod-icon> | ||||
|                             </ion-col> | ||||
|                             <ion-col class="addon-block-timeline-activity-name ion-no-padding"> | ||||
|                                 <p class="item-heading"> | ||||
|                                     <core-format-text [text]="event.activityname || event.name" contextLevel="module" | ||||
|                                         [contextInstanceId]="event.id" [courseId]="event.course && event.course.id"> | ||||
|                                     </core-format-text> | ||||
|                                     <ion-badge *ngIf="event.overdue" color="danger">{{ 'addon.block_timeline.overdue' | translate }} | ||||
|                                     </ion-badge> | ||||
|                                 </p> | ||||
|                                 <p *ngIf="(showCourse && event.course) || event.activitystr"> | ||||
|                                     <span *ngIf="showCourse && event.course"> | ||||
|                                         <core-format-text [text]="event.course.fullnamedisplay" contextLevel="course" | ||||
|                                             [contextInstanceId]="event.course.id"> | ||||
|                                         </core-format-text> · | ||||
|                                     </span> | ||||
|                                     <core-format-text [text]="event.activitystr" contextLevel="module" [contextInstanceId]="event.id"> | ||||
|                                     </core-format-text> | ||||
|                                 </p> | ||||
|                             </ion-col> | ||||
|                         </ion-row> | ||||
|                     </ion-col> | ||||
|                     <ion-col class="addon-block-timeline-activity-action ion-no-padding"> | ||||
|                         <ion-button fill="clear" (click)="action($event, event.action.url)" [title]="event.action.name" | ||||
|                             [disabled]="!event.action.actionable" *ngIf="event.action"> | ||||
|                             {{event.action.name}} | ||||
|                             <ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount"> | ||||
|                                 {{event.action.itemcount}} | ||||
|                             </ion-badge> | ||||
|                         </ion-button> | ||||
|                     </ion-col> | ||||
|                 </ion-row> | ||||
|             </ion-label> | ||||
| 
 | ||||
|             <div slot="end" class="events-info"> | ||||
|                 <div> | ||||
|                     <ion-badge color="light">{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</ion-badge> | ||||
|                 </div> | ||||
|                 <ion-button | ||||
|                     class="ion-hide-md-down" | ||||
|                     fill="clear" | ||||
|                     (click)="action($event, event.action.url)" | ||||
|                     [title]="event.action.name" | ||||
|                     [disabled]="!event.action.actionable" *ngIf="event.action" | ||||
|                 > | ||||
|                     {{event.action.name}} | ||||
|                     <ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount"> | ||||
|                         {{event.action.itemcount}} | ||||
|                     </ion-badge> | ||||
|                 </ion-button> | ||||
|             </div> | ||||
|         </ion-item> | ||||
|     </ng-container> | ||||
| </ion-item-group> | ||||
| @ -57,6 +68,10 @@ | ||||
|     <ion-spinner *ngIf="loadingMore" [attr.aria-label]="'core.loading' | translate"></ion-spinner> | ||||
| </div> | ||||
| 
 | ||||
| <core-empty-box *ngIf="empty" image="assets/img/icons/activities.svg" [message]="'addon.block_timeline.noevents' | translate" | ||||
|     inline="true"> | ||||
| </core-empty-box> | ||||
| <ion-item lines="none" *ngIf="empty && course"> | ||||
|     <ion-label class="ion-text-wrap"> | ||||
|         <p>{{'addon.block_timeline.noevents' | translate}}</p> | ||||
|     </ion-label> | ||||
| </ion-item> | ||||
| <core-empty-box *ngIf="empty && !course" image="assets/img/icons/activities.svg" inline="true" | ||||
|     [message]="'addon.block_timeline.noevents' | translate"></core-empty-box> | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
| @ -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<void>; // 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<void> { | ||||
|         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<number, AddonCalendarEvent[]> = {}; | ||||
|                 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<AddonBlockTimelineEvent[]> { | ||||
|         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<void> { | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
|     async action(event: Event, url: string): Promise<void> { | ||||
|         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; | ||||
| }; | ||||
|  | ||||
| @ -1,5 +1,7 @@ | ||||
| <ion-item-divider sticky="true"> | ||||
|     <ion-label><h2>{{ 'addon.block_timeline.pluginname' | translate }}</h2></ion-label> | ||||
|     <ion-label> | ||||
|         <h2>{{ 'addon.block_timeline.pluginname' | translate }}</h2> | ||||
|     </ion-label> | ||||
|     <core-context-menu slot="end"> | ||||
|         <core-context-menu-item *ngIf="loaded" [priority]="900" [content]="'addon.block_timeline.sortbydates' | translate" | ||||
|             (action)="switchSort('sortbydates')" [iconAction]="sort == 'sortbydates' ? 'far-dot-circle' : 'far-circle'"> | ||||
| @ -18,7 +20,7 @@ | ||||
|             <ion-select-option class="ion-text-wrap" value="overdue"> | ||||
|                 {{ 'addon.block_timeline.overdue' | translate }} | ||||
|             </ion-select-option> | ||||
|             <ion-select-option class="ion-text-wrap" disabled value="disabled"> | ||||
|             <ion-select-option class="ion-text-wrap core-select-option-title" disabled value="disabled"> | ||||
|                 {{ 'addon.block_timeline.duedate' | translate }} | ||||
|             </ion-select-option> | ||||
|             <ion-select-option class="ion-text-wrap" value="next7days"> | ||||
| @ -36,21 +38,14 @@ | ||||
|         </core-combobox> | ||||
|     </div> | ||||
|     <core-loading [hideUntil]="timeline.loaded" [hidden]="sort != 'sortbydates'" [fullscreen]="false"> | ||||
|         <addon-block-timeline-events [events]="timeline.events" showCourse="true" [canLoadMore]="timeline.canLoadMore" | ||||
|             (loadMore)="loadMoreTimeline()" [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events> | ||||
|         <addon-block-timeline-events [events]="timeline.events" [canLoadMore]="timeline.canLoadMore" (loadMore)="loadMore()" | ||||
|             [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events> | ||||
|     </core-loading> | ||||
|     <core-loading [hideUntil]="timelineCourses.loaded" [hidden]="sort != 'sortbycourses'" | ||||
|         [fullscreen]="false" class="safe-area-padding"> | ||||
|         <ion-grid class="ion-no-padding"> | ||||
|             <ion-row class="ion-no-padding"> | ||||
|                 <ion-col *ngFor="let course of timelineCourses.courses" class="ion-no-padding" size="12" size-md="6"> | ||||
|                     <core-courses-course-progress [course]="course"> | ||||
|                         <addon-block-timeline-events [events]="course.events" [canLoadMore]="course.canLoadMore" | ||||
|                             (loadMore)="loadMoreCourse(course)" [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events> | ||||
|                     </core-courses-course-progress> | ||||
|                 </ion-col> | ||||
|             </ion-row> | ||||
|         </ion-grid> | ||||
|     <core-loading [hideUntil]="timelineCourses.loaded" [hidden]="sort != 'sortbycourses'" [fullscreen]="false" class="safe-area-page"> | ||||
|         <ng-container *ngFor="let course of timelineCourses.courses"> | ||||
|             <addon-block-timeline-events [events]="course.events" [canLoadMore]="course.canLoadMore" (loadMore)="loadMore(course)" | ||||
|                 [course]="course" [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events> | ||||
|         </ng-container> | ||||
|         <core-empty-box *ngIf="timelineCourses.courses.length == 0" image="assets/img/icons/courses.svg" inline="true" | ||||
|             [message]="'addon.block_timeline.nocoursesinprogress' | translate"></core-empty-box> | ||||
|     </core-loading> | ||||
|  | ||||
| @ -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<void> { | ||||
|         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<void> { | ||||
|         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<void> { | ||||
|     async loadMore(course?: AddonBlockTimelineCourse): Promise<void> { | ||||
|         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; | ||||
| }; | ||||
|  | ||||
| @ -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.
 | ||||
|  | ||||
| @ -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, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -7,4 +7,8 @@ | ||||
|     ion-item-divider { | ||||
|         min-height: var(--item-divider-min-height); | ||||
|     } | ||||
| 
 | ||||
|     ::ng-deep core-loading { | ||||
|         --loading-inline-min-height: 44px; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,14 +1,12 @@ | ||||
| <ion-card [attr.course-color]="course.color ? null : course.colorNumber"> | ||||
|     <div (click)="openCourse()" class="core-course-thumb" [class.core-course-color-img]="course.courseImage" | ||||
|         [style.background-color]="course.color"> | ||||
|         <img *ngIf="course.courseImage" [src]="course.courseImage" core-external-content alt=""/> | ||||
|         <img *ngIf="course.courseImage" [src]="course.courseImage" core-external-content alt="" /> | ||||
|     </div> | ||||
|     <ion-item button lines="none" (click)="openCourse()" [attr.aria-label]="course.displayname || course.fullname" | ||||
|         class="core-course-header" [class.item-disabled]="course.visible == 0" | ||||
|         [class.core-course-only-title]="!showAll || progress < 0 && completionUserTracked === false" | ||||
|         detail="false"> | ||||
|         <ion-label | ||||
|             class="ion-text-wrap core-course-title" | ||||
|         [class.core-course-only-title]="!showAll || progress < 0 && completionUserTracked === false" detail="false"> | ||||
|         <ion-label class="ion-text-wrap core-course-title" | ||||
|             [class.core-course-with-buttons]="courseOptionMenuEnabled || (downloadCourseEnabled && showDownload)" | ||||
|             [class.core-course-with-spinner]="(downloadCourseEnabled && prefetchCourseData.icon == 'spinner') || showSpinner"> | ||||
|             <p *ngIf="course.categoryname || (course.displayname && course.shortname && course.fullname != course.displayname)" | ||||
| @ -19,8 +17,7 @@ | ||||
|                 </span> | ||||
|                 <span *ngIf="course.categoryname && course.displayname && course.shortname && course.fullname != course.displayname" | ||||
|                     class="core-course-category"> | </span> | ||||
|                 <span *ngIf="course.displayname && course.shortname && course.fullname != course.displayname" | ||||
|                     class="core-course-shortname"> | ||||
|                 <span *ngIf="course.displayname && course.shortname && course.fullname != course.displayname" class="core-course-shortname"> | ||||
|                     <core-format-text [text]="course.shortname" contextLevel="course" [contextInstanceId]="course.id"> | ||||
|                     </core-format-text> | ||||
|                 </span> | ||||
| @ -35,12 +32,8 @@ | ||||
|         </ion-label> | ||||
| 
 | ||||
|         <div class="core-button-spinner" *ngIf="downloadCourseEnabled && !courseOptionMenuEnabled && showDownload" slot="end"> | ||||
|             <core-download-refresh | ||||
|                 [status]="prefetchCourseData.status" | ||||
|                 [enabled]="downloadCourseEnabled" | ||||
|                 [statusTranslatable]="prefetchCourseData.statusTranslatable" | ||||
|                 canTrustDownload="false" | ||||
|                 [loading]="prefetchCourseData.loading" | ||||
|             <core-download-refresh [status]="prefetchCourseData.status" [enabled]="downloadCourseEnabled" | ||||
|                 [statusTranslatable]="prefetchCourseData.statusTranslatable" canTrustDownload="false" [loading]="prefetchCourseData.loading" | ||||
|                 (action)="prefetchCourse()"></core-download-refresh> | ||||
|         </div> | ||||
| 
 | ||||
| @ -50,9 +43,8 @@ | ||||
|                 [attr.aria-label]="'core.loading' | translate"></ion-spinner> | ||||
| 
 | ||||
|             <!-- Downloaded icon. --> | ||||
|             <ion-icon *ngIf="downloadCourseEnabled && prefetchCourseData.downloadSucceeded && !showSpinner" | ||||
|                 class="core-icon-downloaded" name="cloud-done" color="success" role="status" | ||||
|                 [attr.aria-label]="'core.downloaded' | translate"></ion-icon> | ||||
|             <ion-icon *ngIf="downloadCourseEnabled && prefetchCourseData.downloadSucceeded && !showSpinner" class="core-icon-downloaded" | ||||
|                 name="cloud-done" color="success" role="status" [attr.aria-label]="'core.downloaded' | translate"></ion-icon> | ||||
| 
 | ||||
|             <!-- Options menu. --> | ||||
|             <ion-button fill="clear" color="dark" (click)="showCourseOptionsMenu($event)" *ngIf="!showSpinner" | ||||
| @ -61,11 +53,9 @@ | ||||
|             </ion-button> | ||||
|         </div> | ||||
|     </ion-item> | ||||
|     <ion-item *ngIf="showAll && progress >= 0 && completionUserTracked !== false" lines="none" | ||||
|         class="core-course-progress"> | ||||
|     <ion-item *ngIf="showAll && progress >= 0 && completionUserTracked !== false" lines="none" class="core-course-progress"> | ||||
|         <ion-label> | ||||
|             <core-progress-bar [progress]="progress" a11yText="core.courses.aria:courseprogress"></core-progress-bar> | ||||
|         </ion-label> | ||||
|     </ion-item> | ||||
|     <ng-content></ng-content> | ||||
| </ion-card> | ||||
|  | ||||
| @ -9,9 +9,9 @@ | ||||
|         <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}}" | ||||
|             [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-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 *ngIf="tab.badge && tab.badgeA11yText" class="sr-only"> | ||||
|                 {{ tab.badgeA11yText | translate: {$a : tab.badge } }} | ||||
| @ -20,9 +20,10 @@ | ||||
| 
 | ||||
|         <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"> | ||||
|             <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> | ||||
|             <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-bar> | ||||
| </ion-tabs> | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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<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 { 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<CorePushNotificationsNotificationBasicData>( | ||||
|             CorePushNotificationsProvider.COMPONENT, | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user