diff --git a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts index de9d5a8c9..4d31b6553 100644 --- a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts +++ b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts @@ -15,7 +15,12 @@ import { Component, OnInit, OnDestroy, Input, OnChanges, SimpleChange } from '@angular/core'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreSites } from '@services/sites'; -import { CoreCoursesProvider, CoreCoursesMyCoursesUpdatedEventData, CoreCourses } from '@features/courses/services/courses'; +import { + CoreCoursesProvider, + CoreCoursesMyCoursesUpdatedEventData, + CoreCourses, + CoreCourseSummaryData, +} from '@features/courses/services/courses'; import { CoreCourseSearchedDataWithExtraInfoAndOptions, CoreCoursesHelper } from '@features/courses/services/courses-helper'; import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; @@ -35,7 +40,7 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom @Input() downloadEnabled = false; - courses: CoreCourseSearchedDataWithExtraInfoAndOptions[] = []; + courses: (Omit & CoreCourseSearchedDataWithExtraInfoAndOptions)[] = []; prefetchCoursesData: CorePrefetchStatusInfo = { icon: '', statusTranslatable: 'core.loading', @@ -52,7 +57,6 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom protected isDestroyed = false; protected coursesObserver?: CoreEventObserver; protected updateSiteObserver?: CoreEventObserver; - protected courseIds = []; protected fetchContentDefaultError = 'Error getting recent courses data.'; constructor() { @@ -60,7 +64,7 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom } /** - * Component being initialized. + * @inheritdoc */ async ngOnInit(): Promise { // Generate unique id for scroll element. @@ -82,12 +86,8 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom this.coursesObserver = CoreEvents.on( CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, (data) => { - - if (this.shouldRefreshOnUpdatedEvent(data)) { - this.refreshCourseList(); - } + this.refreshCourseList(data); }, - CoreSites.getCurrentSiteId(), ); @@ -95,7 +95,7 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom } /** - * Detect changes on input properties. + * @inheritdoc */ ngOnChanges(changes: {[name: string]: SimpleChange}): void { if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) { @@ -105,21 +105,35 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom } /** - * Perform the invalidate content function. - * - * @return Resolved when done. + * @inheritdoc */ protected async invalidateContent(): Promise { + const courseIds = this.courses.map((course) => course.id); + + await this.invalidateCourses(courseIds); + } + + /** + * Helper function to invalidate only selected courses. + * + * @param courseIds Course Id array. + * @return Promise resolved when done. + */ + protected async invalidateCourses(courseIds: number[]): Promise { const promises: Promise[] = []; + // Invalidate course completion data. promises.push(CoreCourses.invalidateRecentCourses().finally(() => - // Invalidate course completion data. - CoreUtils.allPromises(this.courseIds.map((courseId) => + CoreUtils.allPromises(courseIds.map((courseId) => AddonCourseCompletion.invalidateCourseCompletion(courseId))))); - promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions()); - if (this.courseIds.length > 0) { - promises.push(CoreCourses.invalidateCoursesByField('ids', this.courseIds.join(','))); + if (courseIds.length == 1) { + promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(courseIds[0])); + } else { + promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions()); + } + if (courseIds.length > 0) { + promises.push(CoreCourses.invalidateCoursesByField('ids', courseIds.join(','))); } await CoreUtils.allPromises(promises).finally(() => { @@ -128,9 +142,7 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom } /** - * Fetch the courses for recent courses. - * - * @return Promise resolved when done. + * @inheritdoc */ protected async fetchContent(): Promise { const showCategories = this.block.configsRecord && this.block.configsRecord.displaycategories && @@ -140,17 +152,17 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom const courseIds = recentCourses.map((course) => course.id); // Get the courses using getCoursesByField to get more info about each course. - const courses: CoreCourseSearchedDataWithExtraInfoAndOptions[] = await CoreCourses.getCoursesByField( - 'ids', - courseIds.join(','), - ); + const courses = await CoreCourses.getCoursesByField('ids', courseIds.join(',')); - // Sort them in the original order. - courses.sort((courseA, courseB) => courseIds.indexOf(courseA.id) - courseIds.indexOf(courseB.id)); + this.courses = recentCourses.map((recentCourse) => { + const course = courses.find((course) => recentCourse.id == course.id); + + return Object.assign(recentCourse, course); + }); // Get course options and extra info. const options = await CoreCourses.getCoursesAdminAndNavOptions(courseIds); - courses.forEach((course) => { + this.courses.forEach((course) => { course.navOptions = options.navOptions[course.id]; course.admOptions = options.admOptions[course.id]; @@ -161,24 +173,9 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom await CoreCoursesHelper.loadCoursesColorAndImage(courses); - this.courses = courses; - this.initPrefetchCoursesIcons(); } - /** - * Refresh the list of courses. - * - * @return Promise resolved when done. - */ - protected async refreshCourseList(): Promise { - CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED); - - await CoreUtils.ignoreErrors(CoreCourses.invalidateRecentCourses()); - - await this.loadContent(true); - } - /** * Initialize the prefetch icon for selected courses. */ @@ -194,44 +191,39 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom } /** - * Whether list should be refreshed based on a EVENT_MY_COURSES_UPDATED event. + * Refresh course list based on a EVENT_MY_COURSES_UPDATED event. * * @param data Event data. - * @return Whether to refresh. + * @return Promise resolved when done. */ - protected shouldRefreshOnUpdatedEvent(data: CoreCoursesMyCoursesUpdatedEventData): boolean { + protected async refreshCourseList(data: CoreCoursesMyCoursesUpdatedEventData): Promise { if (data.action == CoreCoursesProvider.ACTION_ENROL) { // Always update if user enrolled in a course. - return true; + return await this.refreshContent(); } - if (data.action == CoreCoursesProvider.ACTION_VIEW && data.courseId != CoreSites.getCurrentSiteHomeId() && - this.courses[0] && data.courseId != this.courses[0].id) { - // Update list if user viewed a course that isn't the most recent one and isn't site home. - return true; + const courseIndex = this.courses.findIndex((course) => course.id == data.courseId); + const course = this.courses[courseIndex]; + if (data.action == CoreCoursesProvider.ACTION_VIEW && data.courseId != CoreSites.getCurrentSiteHomeId()) { + if (!course) { + // Not found, use WS update. + return await this.refreshContent(); + } + + // Place at the begining. + this.courses.splice(courseIndex, 1); + this.courses.unshift(course); + + await this.invalidateCourses([course.id]); } - if (data.action == CoreCoursesProvider.ACTION_STATE_CHANGED && data.state == CoreCoursesProvider.STATE_FAVOURITE && - data.courseId && this.hasCourse(data.courseId)) { - // Update list if a visible course is now favourite or unfavourite. - return true; + if (data.action == CoreCoursesProvider.ACTION_STATE_CHANGED && + data.state == CoreCoursesProvider.STATE_FAVOURITE && course) { + course.isfavourite = !!data.value; + await this.invalidateCourses([course.id]); + + this.initPrefetchCoursesIcons(); } - - return false; - } - - /** - * Check if a certain course is in the list of courses. - * - * @param courseId Course ID to search. - * @return Whether it's in the list. - */ - protected hasCourse(courseId: number): boolean { - if (!this.courses) { - return false; - } - - return !!this.courses.find((course) => course.id == courseId); } /** @@ -253,7 +245,7 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom } /** - * Component being destroyed. + * @inheritdoc */ ngOnDestroy(): void { this.isDestroyed = true; diff --git a/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts b/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts index b95d8084d..766c695c7 100644 --- a/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts +++ b/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts @@ -52,7 +52,6 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im protected isDestroyed = false; protected coursesObserver?: CoreEventObserver; protected updateSiteObserver?: CoreEventObserver; - protected courseIds: number[] = []; protected fetchContentDefaultError = 'Error getting starred courses data.'; constructor() { @@ -60,7 +59,7 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im } /** - * Component being initialized. + * @inheritdoc */ async ngOnInit(): Promise { // Generate unique id for scroll element. @@ -76,17 +75,12 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite(); this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite(); - }, CoreSites.getCurrentSiteId()); this.coursesObserver = CoreEvents.on( CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, (data) => { - - if (this.shouldRefreshOnUpdatedEvent(data)) { - this.refreshCourseList(); - } - this.refreshContent(); + this.refreshCourseList(data); }, CoreSites.getCurrentSiteId(), @@ -96,7 +90,7 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im } /** - * Detect changes on input properties. + * @inheritdoc */ ngOnChanges(changes: {[name: string]: SimpleChange}): void { if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) { @@ -106,21 +100,35 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im } /** - * Perform the invalidate content function. - * - * @return Resolved when done. + * @inheritdoc */ protected async invalidateContent(): Promise { + const courseIds = this.courses.map((course) => course.id); + + await this.invalidateCourses(courseIds); + } + + /** + * Helper function to invalidate only selected courses. + * + * @param courseIds Course Id array. + * @return Promise resolved when done. + */ + protected async invalidateCourses(courseIds: number[]): Promise { const promises: Promise[] = []; + // Invalidate course completion data. promises.push(CoreCourses.invalidateUserCourses().finally(() => - // Invalidate course completion data. - CoreUtils.allPromises(this.courseIds.map((courseId) => + CoreUtils.allPromises(courseIds.map((courseId) => AddonCourseCompletion.invalidateCourseCompletion(courseId))))); - promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions()); - if (this.courseIds.length > 0) { - promises.push(CoreCourses.invalidateCoursesByField('ids', this.courseIds.join(','))); + if (courseIds.length == 1) { + promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(courseIds[0])); + } else { + promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions()); + } + if (courseIds.length > 0) { + promises.push(CoreCourses.invalidateCoursesByField('ids', courseIds.join(','))); } await CoreUtils.allPromises(promises).finally(() => { @@ -129,54 +137,54 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im } /** - * Fetch the courses. - * - * @return Promise resolved when done. + * @inheritdoc */ protected async fetchContent(): Promise { const showCategories = this.block.configsRecord && this.block.configsRecord.displaycategories && this.block.configsRecord.displaycategories.value == '1'; + // @TODO: Sort won't coincide with website because timemodified is not informed. this.courses = await CoreCoursesHelper.getUserCoursesWithOptions('timemodified', 0, 'isfavourite', showCategories); + this.initPrefetchCoursesIcons(); } /** - * Refresh the list of courses. - * - * @return Promise resolved when done. - */ - protected async refreshCourseList(): Promise { - CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED); - - try { - await CoreCourses.invalidateUserCourses(); - } catch (error) { - // Ignore errors. - } - - await this.loadContent(true); - } - - /** - * Whether list should be refreshed based on a EVENT_MY_COURSES_UPDATED event. + * Refresh course list based on a EVENT_MY_COURSES_UPDATED event. * * @param data Event data. - * @return Whether to refresh. + * @return Promise resolved when done. */ - protected shouldRefreshOnUpdatedEvent(data: CoreCoursesMyCoursesUpdatedEventData): boolean { + protected async refreshCourseList(data: CoreCoursesMyCoursesUpdatedEventData): Promise { if (data.action == CoreCoursesProvider.ACTION_ENROL) { // Always update if user enrolled in a course. // New courses shouldn't be favourite by default, but just in case. - return true; + return await this.refreshContent(); } if (data.action == CoreCoursesProvider.ACTION_STATE_CHANGED && data.state == CoreCoursesProvider.STATE_FAVOURITE) { - // Update list when making a course favourite or not. - return true; - } + const courseIndex = this.courses.findIndex((course) => course.id == data.courseId); + if (courseIndex < 0) { + // Not found, use WS update. Usually new favourite. + return await this.refreshContent(); + } - return false; + const course = this.courses[courseIndex]; + if (data.value === false) { + // Unfavourite, just remove. + this.courses.splice(courseIndex, 1); + } else { + // List is not synced, favourite course and place it at the begining. + course.isfavourite = !!data.value; + + this.courses.splice(courseIndex, 1); + this.courses.unshift(course); + } + + await this.invalidateCourses([course.id]); + this.initPrefetchCoursesIcons(); + + } } /** @@ -212,7 +220,7 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im } /** - * Component being destroyed. + * @inheritdoc */ ngOnDestroy(): void { this.isDestroyed = true; 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 81c32360d..a955b9b69 100644 --- a/src/addons/block/timeline/components/timeline/addon-block-timeline.html +++ b/src/addons/block/timeline/components/timeline/addon-block-timeline.html @@ -47,6 +47,6 @@ [course]="course" [from]="dataFrom" [to]="dataTo"> + [message]="'addon.block_timeline.noevents' | translate"> diff --git a/src/addons/block/timeline/components/timeline/timeline.ts b/src/addons/block/timeline/components/timeline/timeline.ts index c8ef64b77..2eb6979df 100644 --- a/src/addons/block/timeline/components/timeline/timeline.ts +++ b/src/addons/block/timeline/components/timeline/timeline.ts @@ -176,9 +176,15 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen const courseEvents = await AddonBlockTimeline.getActionEventsByCourses(this.courseIds); - this.timelineCourses.courses.forEach((course) => { + this.timelineCourses.courses = this.timelineCourses.courses.filter((course) => { + if (courseEvents[course.id].events.length == 0) { + return false; + } + course.events = courseEvents[course.id].events; course.canLoadMore = courseEvents[course.id].canLoadMore; + + return true; }); } } diff --git a/src/addons/blog/services/handlers/mainmenu.ts b/src/addons/blog/services/handlers/mainmenu.ts index 6b49a07ec..142a3d679 100644 --- a/src/addons/blog/services/handlers/mainmenu.ts +++ b/src/addons/blog/services/handlers/mainmenu.ts @@ -26,7 +26,7 @@ export class AddonBlogMainMenuHandlerService implements CoreMainMenuHandler { static readonly PAGE_NAME = 'blog'; name = 'AddonBlog'; - priority = 450; + priority = 500; /** * @inheritdoc diff --git a/src/addons/calendar/services/handlers/mainmenu.ts b/src/addons/calendar/services/handlers/mainmenu.ts index 2893888a5..209dc0bb1 100644 --- a/src/addons/calendar/services/handlers/mainmenu.ts +++ b/src/addons/calendar/services/handlers/mainmenu.ts @@ -26,7 +26,7 @@ export class AddonCalendarMainMenuHandlerService implements CoreMainMenuHandler static readonly PAGE_NAME = 'calendar'; name = 'AddonCalendar'; - priority = 900; + priority = 800; /** * Check if the handler is enabled on a site level. diff --git a/src/addons/messages/services/handlers/mainmenu.ts b/src/addons/messages/services/handlers/mainmenu.ts index 564f844ba..ea20c90d6 100644 --- a/src/addons/messages/services/handlers/mainmenu.ts +++ b/src/addons/messages/services/handlers/mainmenu.ts @@ -38,7 +38,7 @@ export class AddonMessagesMainMenuHandlerService implements CoreMainMenuHandler, static readonly PAGE_NAME = 'messages'; name = 'AddonMessages'; - priority = 800; + priority = 700; protected handler: CoreMainMenuHandlerToDisplay = { icon: 'fas-comments', diff --git a/src/addons/notifications/services/handlers/mainmenu.ts b/src/addons/notifications/services/handlers/mainmenu.ts index bd4800729..de21bc147 100644 --- a/src/addons/notifications/services/handlers/mainmenu.ts +++ b/src/addons/notifications/services/handlers/mainmenu.ts @@ -33,7 +33,7 @@ export class AddonNotificationsMainMenuHandlerService implements CoreMainMenuHan static readonly PAGE_NAME = 'notifications'; name = 'AddonNotifications'; - priority = 700; + priority = 600; protected handlerData: CoreMainMenuHandlerData = { icon: 'fas-bell', diff --git a/src/core/features/courses/services/handlers/my-courses-mainmenu.ts b/src/core/features/courses/services/handlers/my-courses-mainmenu.ts index 3c3991a5c..8c2c51878 100644 --- a/src/core/features/courses/services/handlers/my-courses-mainmenu.ts +++ b/src/core/features/courses/services/handlers/my-courses-mainmenu.ts @@ -29,7 +29,7 @@ export class CoreCoursesMyCoursesMainMenuHandlerService implements CoreMainMenuH static readonly PAGE_NAME = 'courses'; name = 'CoreCoursesMyCourses'; - priority = 850; + priority = 900; /** * @inheritdoc diff --git a/src/core/features/mainmenu/components/user-menu-button/user-menu-button.scss b/src/core/features/mainmenu/components/user-menu-button/user-menu-button.scss index 15aa50df5..8695620af 100644 --- a/src/core/features/mainmenu/components/user-menu-button/user-menu-button.scss +++ b/src/core/features/mainmenu/components/user-menu-button/user-menu-button.scss @@ -1,3 +1,12 @@ -:host-context(ion-tabs.placement-side div.tabs-inner) { +core-user-avatar { + padding: 0; +} + +:host-context(ion-tabs.placement-side ion-toolbar) { display: none; -} \ No newline at end of file +} + +:host-context(ion-toolbar) core-user-avatar ::ng-deep img { + padding: 2px !important; + border: 1px solid var(--color); +} diff --git a/src/core/features/mainmenu/services/handlers/mainmenu.ts b/src/core/features/mainmenu/services/handlers/mainmenu.ts index 45c6bd8ba..a582228ba 100644 --- a/src/core/features/mainmenu/services/handlers/mainmenu.ts +++ b/src/core/features/mainmenu/services/handlers/mainmenu.ts @@ -13,6 +13,9 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { CoreDashboardHomeHandler } from '@features/courses/services/handlers/dashboard-home'; +import { CoreSiteHomeHomeHandler } from '@features/sitehome/services/handlers/sitehome-home'; +import { CoreSites } from '@services/sites'; import { makeSingleton } from '@singletons'; import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../mainmenu-delegate'; @@ -25,13 +28,18 @@ export class CoreMainMenuHomeHandlerService implements CoreMainMenuHandler { static readonly PAGE_NAME = 'home'; name = 'CoreHome'; - priority = 1100; + priority = 1000; /** * @inheritdoc */ async isEnabled(): Promise { - return true; + const siteId = CoreSites.getCurrentSiteId(); + + const dashboardEnabled = await CoreDashboardHomeHandler.isEnabledForSite(siteId); + const siteHomeEnabled = await CoreSiteHomeHomeHandler.isEnabledForSite(siteId); + + return dashboardEnabled || siteHomeEnabled; } /** diff --git a/src/core/features/tag/services/handlers/mainmenu.ts b/src/core/features/tag/services/handlers/mainmenu.ts index 6a434dccf..1a1a7cf77 100644 --- a/src/core/features/tag/services/handlers/mainmenu.ts +++ b/src/core/features/tag/services/handlers/mainmenu.ts @@ -27,7 +27,7 @@ export class CoreTagMainMenuHandlerService implements CoreMainMenuHandler { static readonly PAGE_NAME = 'tag'; name = 'CoreTag'; - priority = 300; + priority = 400; /** * Check if the handler is enabled on a site level. diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index abbcc0de6..116f125dd 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -1090,7 +1090,7 @@ export class CoreSitesProvider { } /** - * Get the list of sites stored, sorted by URL and full name. + * Get the list of sites stored, sorted by sitename, URL and fullname. * * @param ids IDs of the sites to get. If not defined, return all sites. * @return Promise resolved when the sites are retrieved. @@ -1098,24 +1098,28 @@ export class CoreSitesProvider { async getSortedSites(ids?: string[]): Promise { const sites = await this.getSites(ids); - // Sort sites by url and fullname. + // Sort sites by site name, url and then fullname. sites.sort((a, b) => { - // First compare by site url without the protocol. - const compare = a.siteUrlWithoutProtocol.localeCompare(b.siteUrlWithoutProtocol); + // First compare by site name. + let textA = CoreTextUtils.cleanTags(a.siteName).toLowerCase().trim(); + let textB = CoreTextUtils.cleanTags(b.siteName).toLowerCase().trim(); + let compare = textA.localeCompare(textB); if (compare !== 0) { return compare; } - // If site url is the same, use fullname instead. - const fullNameA = a.fullName?.toLowerCase().trim(); - const fullNameB = b.fullName?.toLowerCase().trim(); - - if (!fullNameA || !fullNameB) { - return 0; + // If site name is the same, use site url without the protocol. + compare = a.siteUrlWithoutProtocol.localeCompare(b.siteUrlWithoutProtocol); + if (compare !== 0) { + return compare; } - return fullNameA.localeCompare(fullNameB); + // Finally use fullname. + textA = a.fullName?.toLowerCase().trim() || ''; + textB = b.fullName?.toLowerCase().trim() || ''; + + return textA.localeCompare(textB); }); return sites;