From 807a50a1df04e8297fc7ca1056420dd4b9885c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 2 Feb 2021 12:31:41 +0100 Subject: [PATCH] MOBILE-3625 blocks: Timeline block --- src/addons/block/block.module.ts | 2 + .../myoverview/addon-block-myoverview.html | 6 +- .../timeline/components/components.module.ts | 51 +++ .../events/addon-block-timeline-events.html | 55 ++++ .../timeline/components/events/events.ts | 154 ++++++++++ .../timeline/addon-block-timeline.html | 44 +++ .../timeline/components/timeline/timeline.ts | 240 +++++++++++++++ src/addons/block/timeline/lang.json | 13 + .../block/timeline/services/block-handler.ts | 62 ++++ .../block/timeline/services/timeline.ts | 290 ++++++++++++++++++ src/addons/block/timeline/timeline.module.ts | 38 +++ src/addons/calendar/services/calendar.ts | 35 +++ src/assets/img/icons/activities.svg | 178 +++++++++++ 13 files changed, 1165 insertions(+), 3 deletions(-) create mode 100644 src/addons/block/timeline/components/components.module.ts create mode 100644 src/addons/block/timeline/components/events/addon-block-timeline-events.html create mode 100644 src/addons/block/timeline/components/events/events.ts create mode 100644 src/addons/block/timeline/components/timeline/addon-block-timeline.html create mode 100644 src/addons/block/timeline/components/timeline/timeline.ts create mode 100644 src/addons/block/timeline/lang.json create mode 100644 src/addons/block/timeline/services/block-handler.ts create mode 100644 src/addons/block/timeline/services/timeline.ts create mode 100644 src/addons/block/timeline/timeline.module.ts create mode 100644 src/assets/img/icons/activities.svg diff --git a/src/addons/block/block.module.ts b/src/addons/block/block.module.ts index bc2bbb55a..0d8ff2d47 100644 --- a/src/addons/block/block.module.ts +++ b/src/addons/block/block.module.ts @@ -38,6 +38,7 @@ import { AddonBlockStarredCoursesModule } from './starredcourses/starredcourses. import { AddonBlockTagsModule } from './tags/tags.module'; import { AddonBlockActivityModulesModule } from './activitymodules/activitymodules.module'; import { AddonBlockRecentlyAccessedItemsModule } from './recentlyaccesseditems/recentlyaccesseditems.module'; +import { AddonBlockTimelineModule } from './timeline/timeline.module'; @NgModule({ declarations: [], @@ -66,6 +67,7 @@ import { AddonBlockRecentlyAccessedItemsModule } from './recentlyaccesseditems/r AddonBlockTagsModule, AddonBlockActivityModulesModule, AddonBlockRecentlyAccessedItemsModule, + AddonBlockTimelineModule, ], providers: [], exports: [], diff --git a/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html b/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html index acf647cf3..f9576466d 100644 --- a/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html +++ b/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html @@ -20,15 +20,15 @@ (onClosed)="switchFilterClosed()"> + (action)="switchSort('fullname')" [iconAction]="sort == 'fullname' ? 'far-dot-circle' : 'far-circle'"> + (action)="switchSort('shortname')" [iconAction]="sort == 'shortname' ? 'far-dot-circle' : 'far-circle'"> + (action)="switchSort('lastaccess')" [iconAction]="sort == 'lastaccess' ? 'far-dot-circle' : 'far-circle'"> diff --git a/src/addons/block/timeline/components/components.module.ts b/src/addons/block/timeline/components/components.module.ts new file mode 100644 index 000000000..d2f4620cd --- /dev/null +++ b/src/addons/block/timeline/components/components.module.ts @@ -0,0 +1,51 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { FormsModule } from '@angular/forms'; + +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'; + +@NgModule({ + declarations: [ + AddonBlockTimelineComponent, + AddonBlockTimelineEventsComponent, + ], + imports: [ + CommonModule, + IonicModule, + FormsModule, + TranslateModule.forChild(), + CoreSharedModule, + CoreCoursesComponentsModule, + CoreCourseComponentsModule, + ], + exports: [ + AddonBlockTimelineComponent, + AddonBlockTimelineEventsComponent, + ], + entryComponents: [ + AddonBlockTimelineComponent, + AddonBlockTimelineEventsComponent, + ], +}) +export class AddonBlockTimelineComponentsModule {} 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 new file mode 100644 index 000000000..67db5d061 --- /dev/null +++ b/src/addons/block/timeline/components/events/addon-block-timeline-events.html @@ -0,0 +1,55 @@ + + +

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

+
+ + + + +

+ + +

+

+ + +

+ + + {{event.action.name}} + {{event.action.itemcount}} + + +
+ + + + {{event.timesort * 1000 | coreFormatDate:"strftimetime24" }} + + + + {{event.action.name}} + {{event.action.itemcount}} + + + + +
+
+
+ +
+ + + {{ 'core.loadmore' | translate }} + + +
+ + diff --git a/src/addons/block/timeline/components/events/events.ts b/src/addons/block/timeline/components/events/events.ts new file mode 100644 index 000000000..0f5a3f97e --- /dev/null +++ b/src/addons/block/timeline/components/events/events.ts @@ -0,0 +1,154 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, Input, Output, OnChanges, EventEmitter, SimpleChange } from '@angular/core'; +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'; + +/** + * Directive to render a list of events in course overview. + */ +@Component({ + selector: 'addon-block-timeline-events', + templateUrl: 'addon-block-timeline-events.html', +}) +export class AddonBlockTimelineEventsComponent implements OnChanges { + + @Input() events: AddonBlockTimelineEvent[] = []; // The events to render. + @Input() showCourse?: boolean | string; // 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. + + empty = true; + loadingMore = false; + filteredEvents: AddonBlockTimelineEventFilteredEvent[] = []; + + constructor() { + this.loadMore = new EventEmitter(); + } + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + this.showCourse = CoreUtils.instance.isTrueOrOne(this.showCourse); + + if (changes.events || changes.from || changes.to) { + if (this.events && this.events.length > 0) { + const filteredEvents = this.filterEventsByTime(this.from, this.to); + this.empty = !filteredEvents || filteredEvents.length <= 0; + + const eventsByDay: Record = {}; + filteredEvents.forEach((event) => { + const dayTimestamp = CoreTimeUtils.instance.getMidnightForTimestamp(event.timesort); + if (eventsByDay[dayTimestamp]) { + eventsByDay[dayTimestamp].push(event); + } else { + eventsByDay[dayTimestamp] = [event]; + } + }); + + const todaysMidnight = CoreTimeUtils.instance.getMidnightForTimestamp(); + this.filteredEvents = []; + Object.keys(eventsByDay).forEach((key) => { + const dayTimestamp = parseInt(key); + this.filteredEvents.push({ + color: dayTimestamp < todaysMidnight ? 'danger' : 'light', + dayTimestamp, + events: eventsByDay[dayTimestamp], + }); + }); + } else { + this.empty = true; + } + } + } + + /** + * Filter the events by time. + * + * @param start Number of days to start getting events from today. E.g. -1 will get events from yesterday. + * @param end Number of days after the start. + * @return Filtered events. + */ + protected filterEventsByTime(start: number, end?: number): AddonBlockTimelineEvent[] { + start = moment().add(start, 'days').startOf('day').unix(); + end = typeof end != 'undefined' ? moment().add(end, 'days').startOf('day').unix() : end; + + return this.events.filter((event) => { + if (end) { + return start <= event.timesort && event.timesort < end; + } + + return start <= event.timesort; + }).map((event) => { + event.iconUrl = CoreCourse.instance.getModuleIconSrc(event.icon.component); + + return event; + }); + } + + /** + * Load more events clicked. + */ + loadMoreEvents(): void { + this.loadingMore = true; + this.loadMore.emit(); + } + + /** + * Action clicked. + * + * @param e Click event. + * @param url Url of the action. + */ + async action(e: Event, url: string): Promise { + e.preventDefault(); + e.stopPropagation(); + + // Fix URL format. + url = CoreTextUtils.instance.decodeHTMLEntities(url); + + const modal = await CoreDomUtils.instance.showModalLoading(); + + try { + const treated = await CoreContentLinksHelper.instance.handleLink(url); + if (!treated) { + return CoreSites.instance.getCurrentSite()?.openInBrowserWithAutoLoginIfSameSite(url); + } + } finally { + modal.dismiss(); + } + } + +} + +type AddonBlockTimelineEvent = AddonCalendarEvent & { + iconUrl?: string; +}; + +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 new file mode 100644 index 000000000..188b94023 --- /dev/null +++ b/src/addons/block/timeline/components/timeline/addon-block-timeline.html @@ -0,0 +1,44 @@ + +

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

+ + + + + + +
+ +
+ + {{ 'core.all' | translate }} + {{ 'addon.block_timeline.overdue' | translate }} + {{ 'addon.block_timeline.duedate' | translate }} + {{ 'addon.block_timeline.next7days' | translate }} + {{ 'addon.block_timeline.next30days' | translate }} + {{ 'addon.block_timeline.next3months' | translate }} + {{ 'addon.block_timeline.next6months' | translate }} + +
+ + + + + + + + + + + + + + + +
diff --git a/src/addons/block/timeline/components/timeline/timeline.ts b/src/addons/block/timeline/components/timeline/timeline.ts new file mode 100644 index 000000000..4f25fcfea --- /dev/null +++ b/src/addons/block/timeline/components/timeline/timeline.ts @@ -0,0 +1,240 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { CoreTimeUtils } from '@services/utils/time'; +import { CoreSites } from '@services/sites'; +import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component'; +import { AddonBlockTimeline } from '../../services/timeline'; +import { AddonCalendarEvent } from '@addons/calendar/services/calendar'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper'; +import { CoreSite } from '@classes/site'; +import { CoreCourses } from '@features/courses/services/courses'; +import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; + +/** + * Component to render a timeline block. + */ +@Component({ + selector: 'addon-block-timeline', + templateUrl: 'addon-block-timeline.html', +}) +export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implements OnInit { + + sort = 'sortbydates'; + filter = 'next30days'; + currentSite?: CoreSite; + timeline: { + events: AddonCalendarEvent[]; + loaded: boolean; + canLoadMore?: number; + } = { + events: [], + loaded: false, + }; + + timelineCourses: { + courses: AddonBlockTimelineCourse[]; + loaded: boolean; + canLoadMore?: number; + } = { + courses: [], + loaded: false, + }; + + dataFrom?: number; + dataTo?: number; + + protected courseIds: number[] = []; + protected fetchContentDefaultError = 'Error getting timeline data.'; + + constructor() { + super('AddonBlockTimelineComponent'); + } + + /** + * Component being initialized. + */ + async ngOnInit(): Promise { + this.currentSite = CoreSites.instance.getCurrentSite(); + + this.filter = await this.currentSite!.getLocalSiteConfig('AddonBlockTimelineFilter', this.filter); + this.switchFilter(); + + this.sort = await this.currentSite!.getLocalSiteConfig('AddonBlockTimelineSort', this.sort); + + super.ngOnInit(); + } + + /** + * Perform the invalidate content function. + * + * @return Resolved when done. + */ + protected invalidateContent(): Promise { + const promises: Promise[] = []; + + promises.push(AddonBlockTimeline.instance.invalidateActionEventsByTimesort()); + promises.push(AddonBlockTimeline.instance.invalidateActionEventsByCourses()); + promises.push(CoreCourses.instance.invalidateUserCourses()); + promises.push(CoreCourseOptionsDelegate.instance.clearAndInvalidateCoursesOptions()); + if (this.courseIds.length > 0) { + promises.push(CoreCourses.instance.invalidateCoursesByField('ids', this.courseIds.join(','))); + } + + return CoreUtils.instance.allPromises(promises); + } + + /** + * Fetch the courses for my overview. + * + * @return Promise resolved when done. + */ + protected async fetchContent(): Promise { + if (this.sort == 'sortbydates') { + return this.fetchMyOverviewTimeline().finally(() => { + this.timeline.loaded = true; + }); + } + + if (this.sort == 'sortbycourses') { + return this.fetchMyOverviewTimelineByCourses().finally(() => { + this.timelineCourses.loaded = true; + }); + } + } + + /** + * Load more events. + */ + async loadMoreTimeline(): Promise { + try { + await this.fetchMyOverviewTimeline(this.timeline.canLoadMore); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, this.fetchContentDefaultError); + } + } + + /** + * Load more events. + * + * @param course Course. + * @return Promise resolved when done. + */ + async loadMoreCourse(course: AddonBlockTimelineCourse): Promise { + try { + const courseEvents = await AddonBlockTimeline.instance.getActionEventsByCourse(course.id, course.canLoadMore); + course.events = course.events?.concat(courseEvents.events); + course.canLoadMore = courseEvents.canLoadMore; + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, this.fetchContentDefaultError); + } + } + + /** + * Fetch the timeline. + * + * @param afterEventId The last event id. + * @return Promise resolved when done. + */ + protected async fetchMyOverviewTimeline(afterEventId?: number): Promise { + const events = await AddonBlockTimeline.instance.getActionEventsByTimesort(afterEventId); + + this.timeline.events = events.events; + this.timeline.canLoadMore = events.canLoadMore; + } + + /** + * Fetch the timeline by courses. + * + * @return Promise resolved when done. + */ + protected async fetchMyOverviewTimelineByCourses(): Promise { + const courses = await CoreCoursesHelper.instance.getUserCoursesWithOptions(); + const today = CoreTimeUtils.instance.timestamp(); + + this.timelineCourses.courses = courses.filter((course) => + (course.startdate || 0) <= today && (!course.enddate || course.enddate >= today)); + + if (this.timelineCourses.courses.length > 0) { + this.courseIds = this.timelineCourses.courses.map((course) => course.id); + + const courseEvents = await AddonBlockTimeline.instance.getActionEventsByCourses(this.courseIds); + + this.timelineCourses.courses.forEach((course) => { + course.events = courseEvents[course.id].events; + course.canLoadMore = courseEvents[course.id].canLoadMore; + }); + } + } + + /** + * Change timeline filter being viewed. + */ + switchFilter(): void { + this.currentSite?.setLocalSiteConfig('AddonBlockTimelineFilter', this.filter); + + switch (this.filter) { + case 'overdue': + this.dataFrom = -14; + this.dataTo = 0; + break; + case 'next7days': + this.dataFrom = 0; + this.dataTo = 7; + break; + case 'next30days': + this.dataFrom = 0; + this.dataTo = 30; + break; + case 'next3months': + this.dataFrom = 0; + this.dataTo = 90; + break; + case 'next6months': + this.dataFrom = 0; + this.dataTo = 180; + break; + default: + case 'all': + this.dataFrom = -14; + this.dataTo = undefined; + break; + } + } + + /** + * Change timeline sort being viewed. + * + * @param sort New sorting. + */ + switchSort(sort: string): void { + this.sort = sort; + this.currentSite?.setLocalSiteConfig('AddonBlockTimelineSort', this.sort); + + if (!this.timeline.loaded && this.sort == 'sortbydates') { + this.fetchContent(); + } else if (!this.timelineCourses.loaded && this.sort == 'sortbycourses') { + this.fetchContent(); + } + } + +} + +type AddonBlockTimelineCourse = CoreEnrolledCourseDataWithOptions & { + events?: AddonCalendarEvent[]; + canLoadMore?: number; +}; diff --git a/src/addons/block/timeline/lang.json b/src/addons/block/timeline/lang.json new file mode 100644 index 000000000..fbd482f47 --- /dev/null +++ b/src/addons/block/timeline/lang.json @@ -0,0 +1,13 @@ +{ + "duedate": "Due date", + "next30days": "Next 30 days", + "next3months": "Next 3 months", + "next6months": "Next 6 months", + "next7days": "Next 7 days", + "nocoursesinprogress": "No in-progress courses", + "noevents": "No upcoming activities due", + "overdue": "Overdue", + "pluginname": "Timeline", + "sortbycourses": "Sort by courses", + "sortbydates": "Sort by dates" +} \ No newline at end of file diff --git a/src/addons/block/timeline/services/block-handler.ts b/src/addons/block/timeline/services/block-handler.ts new file mode 100644 index 000000000..277fd6dc7 --- /dev/null +++ b/src/addons/block/timeline/services/block-handler.ts @@ -0,0 +1,62 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreSites } from '@services/sites'; +import { CoreBlockHandlerData } from '@features/block/services/block-delegate'; +import { CoreCourses } from '@features/courses/services/courses'; +import { AddonBlockTimelineComponent } from '@addons/block/timeline/components/timeline/timeline'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { makeSingleton } from '@singletons'; +import { AddonBlockTimeline } from './timeline'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockTimelineHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockTimeline'; + blockName = 'timeline'; + + /** + * Check if the handler is enabled on a site level. + * + * @return Whether or not the handler is enabled on a site level. + */ + async isEnabled(): Promise { + const enabled = await AddonBlockTimeline.instance.isAvailable(); + const currentSite = CoreSites.instance.getCurrentSite(); + + return enabled && ((currentSite && currentSite.isVersionGreaterEqualThan('3.6')) || + !CoreCourses.instance.isMyCoursesDisabledInSite()); + } + + /** + * Returns the data needed to render the block. + * + * @return Data or promise resolved with the data. + */ + getDisplayData(): CoreBlockHandlerData { + + return { + title: 'addon.block_timeline.pluginname', + class: 'addon-block-timeline', + component: AddonBlockTimelineComponent, + }; + } + +} + +export class AddonBlockTimelineHandler extends makeSingleton(AddonBlockTimelineHandlerService) {} diff --git a/src/addons/block/timeline/services/timeline.ts b/src/addons/block/timeline/services/timeline.ts new file mode 100644 index 000000000..3848dbd1a --- /dev/null +++ b/src/addons/block/timeline/services/timeline.ts @@ -0,0 +1,290 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreSites } from '@services/sites'; +import { CoreCoursesDashboard } from '@features/courses/services/dashboard'; +import { + AddonCalendarEvents, + AddonCalendarEventsGroupedByCourse, + AddonCalendarEvent, + AddonCalendarGetActionEventsByCourseWSParams, + AddonCalendarGetActionEventsByTimesortWSParams, + AddonCalendarGetActionEventsByCoursesWSParams, +} from '@addons/calendar/services/calendar'; +import moment from 'moment'; +import { makeSingleton } from '@singletons'; +import { CoreSiteWSPreSets } from '@classes/site'; +import { CoreError } from '@classes/errors/error'; + +// Cache key was maintained from block myoverview when blocks were splitted. +const ROOT_CACHE_KEY = 'myoverview:'; + +/** + * Service that provides some features regarding course overview. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockTimelineProvider { + + static readonly EVENTS_LIMIT = 20; + static readonly EVENTS_LIMIT_PER_COURSE = 10; + + /** + * Get calendar action events for the given course. + * + * @param courseId Only events in this course. + * @param afterEventId The last seen event id. + * @param siteId Site ID. If not defined, use current site. + * @return Promise resolved when the info is retrieved. + */ + async getActionEventsByCourse( + courseId: number, + afterEventId?: number, + siteId?: string, + ): Promise<{ events: AddonCalendarEvent[]; canLoadMore?: number }> { + const site = await CoreSites.instance.getSite(siteId); + + const time = moment().subtract(14, 'days').unix(); // Check two weeks ago. + + const data: AddonCalendarGetActionEventsByCourseWSParams = { + timesortfrom: time, + courseid: courseId, + limitnum: AddonBlockTimelineProvider.EVENTS_LIMIT_PER_COURSE, + }; + if (afterEventId) { + data.aftereventid = afterEventId; + } + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getActionEventsByCourseCacheKey(courseId), + }; + + const courseEvents = await site.read( + 'core_calendar_get_action_events_by_course', + data, + preSets, + ); + + if (courseEvents && courseEvents.events) { + return this.treatCourseEvents(courseEvents, time); + } + + throw new CoreError('No events returned on core_calendar_get_action_events_by_course.'); + } + + /** + * Get cache key for get calendar action events for the given course value WS call. + * + * @param courseId Only events in this course. + * @return Cache key. + */ + protected getActionEventsByCourseCacheKey(courseId: number): string { + return this.getActionEventsByCoursesCacheKey() + ':' + courseId; + } + + /** + * Get calendar action events for a given list of courses. + * + * @param courseIds Course IDs. + * @param siteId Site ID. If not defined, use current site. + * @return Promise resolved when the info is retrieved. + */ + async getActionEventsByCourses( + courseIds: number[], + siteId?: string, + ): Promise<{[courseId: string]: { events: AddonCalendarEvent[]; canLoadMore: number } }> { + const site = await CoreSites.instance.getSite(siteId); + + const time = moment().subtract(14, 'days').unix(); // Check two weeks ago. + + const data: AddonCalendarGetActionEventsByCoursesWSParams = { + timesortfrom: time, + courseids: courseIds, + limitnum: AddonBlockTimelineProvider.EVENTS_LIMIT_PER_COURSE, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getActionEventsByCoursesCacheKey(), + }; + + const events = await site.read( + 'core_calendar_get_action_events_by_courses', + data, + preSets, + ); + + if (events && events.groupedbycourse) { + const courseEvents = {}; + + events.groupedbycourse.forEach((course) => { + courseEvents[course.courseid] = this.treatCourseEvents(course, time); + }); + + return courseEvents; + } + + throw new CoreError('No events returned on core_calendar_get_action_events_by_courses.'); + } + + /** + * Get cache key for get calendar action events for a given list of courses value WS call. + * + * @return Cache key. + */ + protected getActionEventsByCoursesCacheKey(): string { + return ROOT_CACHE_KEY + 'bycourse'; + } + + /** + * Get calendar action events based on the timesort value. + * + * @param afterEventId The last seen event id. + * @param siteId Site ID. If not defined, use current site. + * @return Promise resolved when the info is retrieved. + */ + async getActionEventsByTimesort( + afterEventId?: number, + siteId?: string, + ): Promise<{ events: AddonCalendarEvent[]; canLoadMore?: number }> { + const site = await CoreSites.instance.getSite(siteId); + + const timesortfrom = moment().subtract(14, 'days').unix(); // Check two weeks ago. + const limitnum = AddonBlockTimelineProvider.EVENTS_LIMIT; + + const data: AddonCalendarGetActionEventsByTimesortWSParams = { + timesortfrom, + limitnum, + }; + if (afterEventId) { + data.aftereventid = afterEventId; + } + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getActionEventsByTimesortCacheKey(afterEventId, limitnum), + getCacheUsingCacheKey: true, + uniqueCacheKey: true, + }; + + const result = await site.read( + 'core_calendar_get_action_events_by_timesort', + data, + preSets, + ); + + if (result && result.events) { + const canLoadMore = result.events.length >= limitnum ? result.lastid : undefined; + + // Filter events by time in case it uses cache. + const events = result.events.filter((element) => element.timesort >= timesortfrom); + + return { + events, + canLoadMore, + }; + } + + throw new CoreError('No events returned on core_calendar_get_action_events_by_timesort.'); + } + + /** + * Get prefix cache key for calendar action events based on the timesort value WS calls. + * + * @return Cache key. + */ + protected getActionEventsByTimesortPrefixCacheKey(): string { + return ROOT_CACHE_KEY + 'bytimesort:'; + } + + /** + * Get cache key for get calendar action events based on the timesort value WS call. + * + * @param afterEventId The last seen event id. + * @param limit Limit num of the call. + * @return Cache key. + */ + protected getActionEventsByTimesortCacheKey(afterEventId?: number, limit?: number): string { + afterEventId = afterEventId || 0; + limit = limit || 0; + + return this.getActionEventsByTimesortPrefixCacheKey() + afterEventId + ':' + limit; + } + + /** + * Invalidates get calendar action events for a given list of courses WS call. + * + * @param siteId Site ID to invalidate. If not defined, use current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateActionEventsByCourses(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKeyStartingWith(this.getActionEventsByCoursesCacheKey()); + } + + /** + * Invalidates get calendar action events based on the timesort value WS call. + * + * @param siteId Site ID to invalidate. If not defined, use current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateActionEventsByTimesort(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKeyStartingWith(this.getActionEventsByTimesortPrefixCacheKey()); + } + + /** + * Returns whether or not My Overview is available for a certain site. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with true if available, resolved with false or rejected otherwise. + */ + async isAvailable(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + // First check if dashboard is disabled. + if (CoreCoursesDashboard.instance.isDisabledInSite(site)) { + return false; + } + + return site.wsAvailable('core_calendar_get_action_events_by_courses') && + site.wsAvailable('core_calendar_get_action_events_by_timesort'); + } + + /** + * Handles course events, filtering and treating if more can be loaded. + * + * @param course Object containing response course events info. + * @param timeFrom Current time to filter events from. + * @return Object with course events and last loaded event id if more can be loaded. + */ + protected treatCourseEvents( + course: AddonCalendarEvents, + timeFrom: number, + ): { events: AddonCalendarEvent[]; canLoadMore?: number } { + + const canLoadMore: number | undefined = + course.events.length >= AddonBlockTimelineProvider.EVENTS_LIMIT_PER_COURSE ? course.lastid : undefined; + + // Filter events by time in case it uses cache. + course.events = course.events.filter((element) => element.timesort >= timeFrom); + + return { + events: course.events, + canLoadMore, + }; + } + +} + +export class AddonBlockTimeline extends makeSingleton(AddonBlockTimelineProvider) {} diff --git a/src/addons/block/timeline/timeline.module.ts b/src/addons/block/timeline/timeline.module.ts new file mode 100644 index 000000000..3c024d20c --- /dev/null +++ b/src/addons/block/timeline/timeline.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockTimelineComponentsModule } from './components/components.module'; +import { AddonBlockTimelineHandler } from './services/block-handler'; + +@NgModule({ + imports: [ + IonicModule, + AddonBlockTimelineComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockTimelineHandler.instance); + }, + }, + ], +}) +export class AddonBlockTimelineModule {} diff --git a/src/addons/calendar/services/calendar.ts b/src/addons/calendar/services/calendar.ts index 4644dded1..fe7e1d6a1 100644 --- a/src/addons/calendar/services/calendar.ts +++ b/src/addons/calendar/services/calendar.ts @@ -1749,6 +1749,7 @@ export class AddonCalendar extends makeSingleton(AddonCalendarProvider) {} /** * Data returned by calendar's events_exporter. + * Data returned by core_calendar_get_action_events_by_course and core_calendar_get_action_events_by_timesort WS. */ export type AddonCalendarEvents = { events: AddonCalendarEvent[]; // Events. @@ -1756,13 +1757,47 @@ export type AddonCalendarEvents = { lastid: number; // Lastid. }; +/** + * Params of core_calendar_get_action_events_by_courses WS. + */ +export type AddonCalendarGetActionEventsByCoursesWSParams = { + courseids: number[]; + timesortfrom?: number; // Time sort from. + timesortto?: number; // Time sort to. + limitnum?: number; // Limit number. +}; + /** * Data returned by calendar's events_grouped_by_course_exporter. + * Data returned by core_calendar_get_action_events_by_courses WS. */ export type AddonCalendarEventsGroupedByCourse = { groupedbycourse: AddonCalendarEventsSameCourse[]; // Groupped by course. }; +/** + * Params of core_calendar_get_action_events_by_course WS. + */ +export type AddonCalendarGetActionEventsByCourseWSParams = { + courseid: number; // Course id. + timesortfrom?: number; // Time sort from. + timesortto?: number; // Time sort to. + aftereventid?: number; // The last seen event id. + limitnum?: number; // Limit number. +}; + +/** + * Params of core_calendar_get_action_events_by_timesort WS. + */ +export type AddonCalendarGetActionEventsByTimesortWSParams = { + timesortfrom?: number; // Time sort from. + timesortto?: number; // Time sort to. + aftereventid?: number; // The last seen event id. + limitnum?: number; // Limit number. + limittononsuspendedevents?: boolean; // Limit the events to courses the user is not suspended in. + userid?: number; // The user id. +}; + /** * Data returned by calendar's events_same_course_exporter. */ diff --git a/src/assets/img/icons/activities.svg b/src/assets/img/icons/activities.svg new file mode 100644 index 000000000..56243a53c --- /dev/null +++ b/src/assets/img/icons/activities.svg @@ -0,0 +1,178 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + +