From d527695977bf58f1ed5f4417be4c96dd6a87c64c Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 10 Dec 2021 14:50:55 +0100 Subject: [PATCH] MOBILE-3927 calendar: Add swipe to calendar daily view --- .../calendar/components/calendar/calendar.ts | 136 ++- src/addons/calendar/pages/day/day.html | 132 +-- src/addons/calendar/pages/day/day.page.ts | 781 ++++++++++-------- src/addons/calendar/pages/day/day.scss | 4 + src/addons/calendar/pages/index/index.page.ts | 16 +- .../calendar/services/calendar-helper.ts | 26 +- .../slides-dynamic-items-manager-source.ts | 84 +- .../slides-dynamic-items-manager.ts | 4 +- .../components/swipe-slides/swipe-slides.scss | 6 + src/core/services/utils/time.ts | 121 ++- 10 files changed, 800 insertions(+), 510 deletions(-) diff --git a/src/addons/calendar/components/calendar/calendar.ts b/src/addons/calendar/components/calendar/calendar.ts index 6a0f3a4c5..36614d3d5 100644 --- a/src/addons/calendar/components/calendar/calendar.ts +++ b/src/addons/calendar/components/calendar/calendar.ts @@ -43,7 +43,10 @@ import { CoreCategoryData, CoreCourses } from '@features/courses/services/course import { CoreApp } from '@services/app'; import { CoreLocalNotifications } from '@services/local-notifications'; import { CoreSwipeCurrentItemData, CoreSwipeSlidesComponent } from '@components/swipe-slides/swipe-slides'; -import { CoreSwipeSlidesDynamicItemsManagerSource } from '@classes/items-management/slides-dynamic-items-manager-source'; +import { + CoreSwipeSlidesDynamicItem, + CoreSwipeSlidesDynamicItemsManagerSource, +} from '@classes/items-management/slides-dynamic-items-manager-source'; import { CoreSwipeSlidesDynamicItemsManager } from '@classes/items-management/slides-dynamic-items-manager'; /** @@ -113,8 +116,8 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro this.undeleteEvent(data.eventId); // Remove it from the list of deleted events if it's there. - const index = this.manager?.getSource().deletedEvents.indexOf(data.eventId); - if (index !== undefined && index != -1) { + const index = this.manager?.getSource().deletedEvents.indexOf(data.eventId) ?? -1; + if (index != -1) { this.manager?.getSource().deletedEvents.splice(index, 1); } }, @@ -128,15 +131,15 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro * Component loaded. */ ngOnInit(): void { - const now = new Date(); + const currentMonth = CoreTimeUtils.getCurrentMonth(); this.canNavigate = typeof this.canNavigate == 'undefined' ? true : CoreUtils.isTrueOrOne(this.canNavigate); this.displayNavButtons = typeof this.displayNavButtons == 'undefined' ? true : CoreUtils.isTrueOrOne(this.displayNavButtons); const source = new AddonCalendarMonthSlidesItemsManagerSource(this, { - year: this.initialYear ?? now.getFullYear(), - monthNumber: this.initialMonth ?? now.getMonth() + 1, + year: this.initialYear ?? currentMonth.year, + monthNumber: this.initialMonth ?? currentMonth.monthNumber, }); this.manager = new CoreSwipeSlidesDynamicItemsManager(source); @@ -157,7 +160,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro if (changes) { items.forEach((month) => { if (month.loaded) { - this.filterEvents(month.weeks); + this.manager?.getSource().filterEvents(month.weeks, this.filter); } }); } @@ -177,7 +180,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro try { await this.manager?.getSource().fetchData(); - await this.manager?.getSource().load(); + await this.manager?.getSource().load(this.manager?.getSelectedItem()); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); } @@ -186,11 +189,9 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro } /** - * Load current month and preload next and previous ones. - * - * @return Promise resolved when done. + * Update data related to month being viewed. */ - async viewMonth(month: YearAndMonth): Promise { + viewMonth(month: YearAndMonth): void { // Calculate the period name. We don't use the one in result because it's in server's language. this.periodName = CoreTimeUtils.userDate( new Date(month.year, month.monthNumber - 1).getTime(), @@ -199,35 +200,19 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro this.calculateIsCurrentMonth(); } - /** - * Filter events based on the filter popover. - * - * @param weeks Weeks with the events to filter. - */ - filterEvents(weeks: AddonCalendarWeek[]): void { - weeks.forEach((week) => { - week.days.forEach((day) => { - day.filteredEvents = AddonCalendarHelper.getFilteredEvents( - day.eventsFormated || [], - this.filter, - this.manager?.getSource().categories || {}, - ); - - // Re-calculate some properties. - AddonCalendarHelper.calculateDayData(day, day.filteredEvents); - }); - }); - } - /** * Refresh events. * * @param afterChange Whether the refresh is done after an event has changed or has been synced. * @return Promise resolved when done. */ - async refreshData(): Promise { + async refreshData(afterChange = false): Promise { const visibleMonth = this.slides?.getCurrentItem() || null; + if (afterChange) { + this.manager?.getSource().markAllItemsDirty(); + } + await this.manager?.getSource().invalidateContent(visibleMonth); await this.fetchData(); @@ -282,8 +267,8 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro } this.currentTime = CoreTimeUtils.timestamp(); - this.isCurrentMonth = CoreTimeUtils.isCurrentMonth(visibleMonth); - this.isPastMonth = CoreTimeUtils.isCurrentMonth(visibleMonth); + this.isCurrentMonth = CoreTimeUtils.isSameMonth(visibleMonth, CoreTimeUtils.getCurrentMonth()); + this.isPastMonth = CoreTimeUtils.isPastMonth(visibleMonth); } /** @@ -305,20 +290,22 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro * @param eventId Event ID. */ protected async undeleteEvent(eventId: number): Promise { - this.manager?.getSource().getItems()?.forEach((month) => { + this.manager?.getSource().getItems()?.some((month) => { if (!month.loaded) { - return; + return false; } - month.weeks.forEach((week) => { - week.days.forEach((day) => { - const event = day.eventsFormated?.find((event) => event.id == eventId); + return month.weeks.some((week) => week.days.some((day) => { + const event = day.eventsFormated?.find((event) => event.id == eventId); - if (event) { - event.deleted = false; - } - }); - }); + if (event) { + event.deleted = false; + + return true; + } + + return false; + })); }); } @@ -354,17 +341,13 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro /** * Preloaded month. */ -type PreloadedMonth = YearAndMonth & { - loaded: boolean; // Whether the events have been loaded. +type PreloadedMonth = YearAndMonth & CoreSwipeSlidesDynamicItem & { weekDays: AddonCalendarWeekDaysTranslationKeys[]; weeks: AddonCalendarWeek[]; - needsRefresh?: boolean; // Whether the events needs to be re-loaded. }; -// CoreSwipeSlidesDynamicItemsManagerSource - /** - * Helper to manage swiping within a collection of chapters. + * Helper to manage swiping within months. */ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicItemsManagerSource { @@ -399,6 +382,27 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI ]); } + /** + * Filter events based on the filter popover. + * + * @param weeks Weeks with the events to filter. + * @param filter Filter to apply. + */ + filterEvents(weeks: AddonCalendarWeek[], filter?: AddonCalendarFilter): void { + weeks.forEach((week) => { + week.days.forEach((day) => { + day.filteredEvents = AddonCalendarHelper.getFilteredEvents( + day.eventsFormated || [], + filter, + this.categories, + ); + + // Re-calculate some properties. + AddonCalendarHelper.calculateDayData(day, day.filteredEvents); + }); + }); + } + /** * Load categories to be able to filter events. * @@ -437,8 +441,7 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI this.offlineEvents = AddonCalendarHelper.classifyIntoMonths(events); // Get the IDs of events edited in offline. - const filtered = events.filter((event) => event.id > 0); - this.offlineEditedEventsIds = filtered.map((event) => event.id); + this.offlineEditedEventsIds = events.filter((event) => event.id > 0).map((event) => event.id); } /** @@ -463,7 +466,7 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI * @inheritdoc */ getItemId(item: YearAndMonth): string | number { - return `${item.year}#${item.monthNumber}`; + return AddonCalendarHelper.getMonthId(item); } /** @@ -484,12 +487,6 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI * @inheritdoc */ async loadItemData(month: YearAndMonth, preload = false): Promise { - // Check if it's already loaded. - const existingMonth = this.getItem(month); - if (existingMonth && ((existingMonth.loaded && !existingMonth.needsRefresh) || preload)) { - return existingMonth; - } - if (!preload) { this.monthLoaded = false; } @@ -515,7 +512,7 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI const weekDays = AddonCalendar.getWeekDays(result.daynames[0].dayno); const weeks = result.weeks as AddonCalendarWeek[]; - const isCurrentMonth = CoreTimeUtils.isCurrentMonth(month); + const isCurrentMonth = CoreTimeUtils.isSameMonth(month, CoreTimeUtils.getCurrentMonth()); const currentDay = new Date().getDate(); let isPast = true; @@ -552,22 +549,11 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI // Merge the online events with offline data. this.mergeEvents(month, weeks); // Filter events by course. - this.calendarComponent.filterEvents(weeks); + this.filterEvents(weeks, this.calendarComponent.filter); } - if (existingMonth) { - // Month already exists, update it. - existingMonth.loaded = !preload; - existingMonth.weeks = weeks; - existingMonth.weekDays = weekDays; - - return existingMonth; - } - - // Add the preloaded month at the right position. return { ...month, - loaded: !preload, weeks, weekDays, }; @@ -586,7 +572,7 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI */ mergeEvents(month: YearAndMonth, weeks: AddonCalendarWeek[]): void { const monthOfflineEvents: { [day: number]: AddonCalendarEventToDisplay[] } = - this.offlineEvents[AddonCalendarHelper.getMonthId(month.year, month.monthNumber)]; + this.offlineEvents[AddonCalendarHelper.getMonthId(month)]; weeks.forEach((week) => { week.days.forEach((day) => { @@ -637,7 +623,7 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI this.categoriesRetrieved = false; // Get categories again. if (visibleMonth) { - visibleMonth.needsRefresh = true; + visibleMonth.dirty = true; } await Promise.all(promises); diff --git a/src/addons/calendar/pages/day/day.html b/src/addons/calendar/pages/day/day.html index ef0fe2306..28b8bf934 100644 --- a/src/addons/calendar/pages/day/day.html +++ b/src/addons/calendar/pages/day/day.html @@ -14,7 +14,7 @@ - @@ -27,70 +27,78 @@ - - - - - - - - - -

{{ periodName }}

-
- - - - - -
-
+ + + + + + + + + + +

{{ periodName }}

+
+ + + + + +
+
- - - - - - {{ 'core.hasdatatosync' | translate:{$a: 'core.day' | translate} }} - - + + + + + + + + {{ 'core.hasdatatosync' | translate:{$a: 'core.day' | translate} }} + + - - + + - - - - - - - - - - {{ 'addon.calendar.type' + event.formattedType | translate }} - {{ event.iconTitle }} - -

- -

-

-
- - - {{ 'core.notsent' | translate }} - - - - {{ 'core.deletedoffline' | translate }} - -
-
-
+ + + + + + + + + + {{ 'addon.calendar.type' + event.formattedType | translate }} + {{ event.iconTitle }} + +

+ +

+

+
+ + + {{ 'core.notsent' | translate }} + + + + {{ 'core.deletedoffline' | translate }} + +
+
+
+
+
+
diff --git a/src/addons/calendar/pages/day/day.page.ts b/src/addons/calendar/pages/day/day.page.ts index 19963cb62..86039c369 100644 --- a/src/addons/calendar/pages/day/day.page.ts +++ b/src/addons/calendar/pages/day/day.page.ts @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; import { IonRefresher } from '@ionic/angular'; import { CoreApp } from '@services/app'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreLocalNotifications } from '@services/local-notifications'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTimeUtils } from '@services/utils/time'; +import { CoreTimeUtils, YearMonthDay } from '@services/utils/time'; import { AddonCalendarProvider, AddonCalendar, @@ -40,6 +40,12 @@ import { Params } from '@angular/router'; import { Subscription } from 'rxjs'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; +import { CoreSwipeSlidesDynamicItemsManager } from '@classes/items-management/slides-dynamic-items-manager'; +import { CoreSwipeCurrentItemData, CoreSwipeSlidesComponent } from '@components/swipe-slides/swipe-slides'; +import { + CoreSwipeSlidesDynamicItem, + CoreSwipeSlidesDynamicItemsManagerSource, +} from '@classes/items-management/slides-dynamic-items-manager-source'; /** * Page that displays the calendar events for a certain day. @@ -51,20 +57,9 @@ import { CoreConstants } from '@/core/constants'; }) export class AddonCalendarDayPage implements OnInit, OnDestroy { - protected currentSiteId: string; - protected year!: number; - protected month!: number; - protected day!: number; - protected categories: { [id: number]: CoreCategoryData } = {}; - protected events: AddonCalendarEventToDisplay[] = []; // Events (both online and offline). - protected onlineEvents: AddonCalendarEventToDisplay[] = []; - protected offlineEvents: { [monthId: string]: { [day: number]: AddonCalendarEventToDisplay[] } } = - {}; // Offline events classified in month & day. + @ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent; - protected offlineEditedEventsIds: number[] = []; // IDs of events edited in offline. - protected deletedEvents: number[] = []; // Events deleted in offline. - protected timeFormat?: string; - protected currentTime!: number; + protected currentSiteId: string; // Observers. protected newEventObserver: CoreEventObserver; @@ -79,16 +74,11 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { protected filterChangedObserver: CoreEventObserver; periodName?: string; - filteredEvents: AddonCalendarEventToDisplay [] = []; - canCreate = false; - courses: Partial[] = []; + manager?: CoreSwipeSlidesDynamicItemsManager; loaded = false; - hasOffline = false; isOnline = false; syncIcon = CoreConstants.ICON_LOADING; isCurrentDay = false; - isPastDay = false; - currentMoment!: moment.Moment; filter: AddonCalendarFilter = { filtered: false, courseId: undefined, @@ -106,7 +96,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { if (CoreLocalNotifications.isAvailable()) { // Re-schedule events if default time changes. this.obsDefaultTimeChange = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => { - AddonCalendar.scheduleEventsNotifications(this.onlineEvents); + this.manager?.getSource().getItems()?.forEach(day => { + AddonCalendar.scheduleEventsNotifications(day.onlineEvents); + }); }, this.currentSiteId); } @@ -115,7 +107,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { if (data && data.eventId) { - this.loaded = false; + this.manager?.getSource().markAllItemsUnloaded(); this.refreshData(true, true); } }, @@ -124,7 +116,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { // Listen for new event discarded event. When it does, reload the data. this.discardedObserver = CoreEvents.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => { - this.loaded = false; + this.manager?.getSource().markAllItemsUnloaded(); this.refreshData(true, true); }, this.currentSiteId); @@ -133,7 +125,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => { if (data && data.eventId) { - this.loaded = false; + this.manager?.getSource().markAllItemsUnloaded(); this.refreshData(true, true); } }, @@ -142,14 +134,16 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { // Refresh data if calendar events are synchronized automatically. this.syncObserver = CoreEvents.on(AddonCalendarSyncProvider.AUTO_SYNCED, () => { - this.loaded = false; + this.manager?.getSource().markAllItemsUnloaded(); this.refreshData(false, true); }, this.currentSiteId); // Refresh data if calendar events are synchronized manually but not by this page. this.manualSyncObserver = CoreEvents.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => { - if (data && (data.source != 'day' || data.year != this.year || data.month != this.month || data.day != this.day)) { - this.loaded = false; + const visibleDay = this.slides?.getCurrentItem(); + if (data && (data.source != 'day' || !visibleDay || data.day === undefined || data.year != visibleDay.year || + data.month != visibleDay.monthNumber || data.day != visibleDay.dayNumber)) { + this.manager?.getSource().markAllItemsUnloaded(); this.refreshData(false, true); } }, this.currentSiteId); @@ -160,10 +154,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { (data) => { if (data && !data.sent) { // Event was deleted in offline. Just mark it as deleted, no need to refresh. - this.hasOffline = this.markAsDeleted(data.eventId, true) || this.hasOffline; - this.deletedEvents.push(data.eventId); + this.manager?.getSource().markAsDeleted(data.eventId, true); } else { - this.loaded = false; + this.manager?.getSource().markAllItemsUnloaded(); this.refreshData(false, true); } }, @@ -179,26 +172,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { } // Mark it as undeleted, no need to refresh. - const found = this.markAsDeleted(data.eventId, false); - - // Remove it from the list of deleted events if it's there. - const index = this.deletedEvents.indexOf(data.eventId); - if (index != -1) { - this.deletedEvents.splice(index, 1); - } - - if (found) { - // The deleted event belongs to current list. Re-calculate "hasOffline". - this.hasOffline = false; - - if (this.events.length != this.onlineEvents.length) { - this.hasOffline = true; - } else { - const event = this.events.find((event) => event.deleted || event.offline); - - this.hasOffline = !!event; - } - } + this.manager?.getSource().markAsDeleted(data.eventId, false); }, this.currentSiteId, ); @@ -209,9 +183,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.filter = data; // Course viewed has changed, check if the user can create events for this course calendar. - this.canCreate = await AddonCalendarHelper.canEditEvents(this.filter.courseId); + await this.manager?.getSource().loadCanCreate(this.filter.courseId); - this.filterEvents(); + this.manager?.getSource().filterAllDayEvents(this.filter); }, ); @@ -240,17 +214,27 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.filter.filtered = typeof this.filter.courseId != 'undefined' || types.some((name) => !this.filter[name]); - const now = new Date(); - this.year = CoreNavigator.getRouteNumberParam('year') || now.getFullYear(); - this.month = CoreNavigator.getRouteNumberParam('month') || (now.getMonth() + 1); - this.day = CoreNavigator.getRouteNumberParam('day') || now.getDate(); + const currentDay = CoreTimeUtils.getCurrentDay(); + const source = new AddonCalendarDaySlidesItemsManagerSource(this, { + year: CoreNavigator.getRouteNumberParam('year') ?? currentDay.year, + monthNumber: CoreNavigator.getRouteNumberParam('month') ?? currentDay.monthNumber, + dayNumber: CoreNavigator.getRouteNumberParam('day') ?? currentDay.dayNumber, + }); + this.manager = new CoreSwipeSlidesDynamicItemsManager(source); - this.calculateCurrentMoment(); this.calculateIsCurrentDay(); this.fetchData(true); } + get canCreate(): boolean { + return this.manager?.getSource().canCreate || false; + } + + get timeFormat(): string { + return this.manager?.getSource().timeFormat || 'core.strftimetime'; + } + /** * Fetch all the data required for the view. * @@ -259,7 +243,6 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { * @return Promise resolved when done. */ async fetchData(sync?: boolean): Promise { - this.syncIcon = CoreConstants.ICON_LOADING; this.isOnline = CoreApp.isOnline(); @@ -268,53 +251,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { } try { - const promises: Promise[] = []; + await this.manager?.getSource().fetchData(this.filter.courseId); - // Load courses for the popover. - promises.push(CoreCoursesHelper.getCoursesForPopover(this.filter.courseId).then((data) => { - this.courses = data.courses; - - return; - })); - - // Get categories. - promises.push(this.loadCategories()); - - // Get offline events. - promises.push(AddonCalendarOffline.getAllEditedEvents().then((offlineEvents) => { - // Classify them by month & day. - this.offlineEvents = AddonCalendarHelper.classifyIntoMonths(offlineEvents); - - // Get the IDs of events edited in offline. - this.offlineEditedEventsIds = offlineEvents.filter((event) => event.id > 0).map((event) => event.id); - - return; - })); - - // Get events deleted in offline. - promises.push(AddonCalendarOffline.getAllDeletedEventsIds().then((ids) => { - this.deletedEvents = ids; - - return; - })); - - // Check if user can create events. - promises.push(AddonCalendarHelper.canEditEvents(this.filter.courseId).then((canEdit) => { - this.canCreate = canEdit; - - return; - })); - - // Get user preferences. - promises.push(AddonCalendar.getCalendarTimeFormat().then((value) => { - this.timeFormat = value; - - return; - })); - - await Promise.all(promises); - - await this.fetchEvents(); + await this.manager?.getSource().load(this.manager?.getSelectedItem()); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); } @@ -324,104 +263,14 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { } /** - * Fetch the events for current day. - * - * @return Promise resolved when done. + * Update data related to day being viewed. */ - async fetchEvents(): Promise { - let result: AddonCalendarCalendarDay; - try { - // Don't pass courseId and categoryId, we'll filter them locally. - result = await AddonCalendar.getDayEvents(this.year, this.month, this.day); - this.onlineEvents = await Promise.all(result.events.map((event) => AddonCalendarHelper.formatEventData(event))); - } catch (error) { - if (CoreApp.isOnline()) { - throw error; - } - // Allow navigating to non-cached days in offline (behave as if using emergency cache). - this.onlineEvents = []; - } - - // Calculate the period name. We don't use the one in result because it's in server's language. + viewDay(day: YearMonthDay): void { this.periodName = CoreTimeUtils.userDate( - new Date(this.year, this.month - 1, this.day).getTime(), + new Date(day.year, day.monthNumber - 1, day.dayNumber).getTime(), 'core.strftimedaydate', ); - - // Schedule notifications for the events retrieved (only future events will be scheduled). - AddonCalendar.scheduleEventsNotifications(this.onlineEvents); - // Merge the online events with offline data. - this.events = this.mergeEvents(); - // Filter events by course. - this.filterEvents(); this.calculateIsCurrentDay(); - // Re-calculate the formatted time so it uses the device date. - const dayTime = this.currentMoment.unix() * 1000; - - const promises = this.events.map((event) => { - event.ispast = this.isPastDay || (this.isCurrentDay && this.isEventPast(event)); - - return AddonCalendar.formatEventTime(event, this.timeFormat, true, dayTime).then((time) => { - event.formattedtime = time; - - return; - }); - }); - - await Promise.all(promises); - } - - /** - * Merge online events with the offline events of that period. - * - * @return Merged events. - */ - protected mergeEvents(): AddonCalendarEventToDisplay[] { - this.hasOffline = false; - - if (!Object.keys(this.offlineEvents).length && !this.deletedEvents.length) { - // No offline events, nothing to merge. - return this.onlineEvents; - } - - const monthOfflineEvents = this.offlineEvents[AddonCalendarHelper.getMonthId(this.year, this.month)]; - const dayOfflineEvents = monthOfflineEvents && monthOfflineEvents[this.day]; - let result = this.onlineEvents; - - if (this.deletedEvents.length) { - // Mark as deleted the events that were deleted in offline. - result.forEach((event) => { - event.deleted = this.deletedEvents.indexOf(event.id) != -1; - - if (event.deleted) { - this.hasOffline = true; - } - }); - } - - if (this.offlineEditedEventsIds.length) { - // Remove the online events that were modified in offline. - result = result.filter((event) => this.offlineEditedEventsIds.indexOf(event.id) == -1); - - if (result.length != this.onlineEvents.length) { - this.hasOffline = true; - } - } - - if (dayOfflineEvents && dayOfflineEvents.length) { - // Add the offline events (either new or edited). - this.hasOffline = true; - result = AddonCalendarHelper.sortEvents(result.concat(dayOfflineEvents)); - } - - return result; - } - - /** - * Filter events based on the filter popover. - */ - protected filterEvents(): void { - this.filteredEvents = AddonCalendarHelper.getFilteredEvents(this.events, this.filter, this.categories); } /** @@ -452,37 +301,12 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { async refreshData(sync?: boolean, afterChange?: boolean): Promise { this.syncIcon = CoreConstants.ICON_LOADING; - const promises: Promise[] = []; + const visibleDay = this.slides?.getCurrentItem() || null; // Don't invalidate day events after a change, it has already been handled. - if (!afterChange) { - promises.push(AddonCalendar.invalidateDayEvents(this.year, this.month, this.day)); - } - promises.push(AddonCalendar.invalidateAllowedEventTypes()); - promises.push(CoreCourses.invalidateCategories(0, true)); - promises.push(AddonCalendar.invalidateTimeFormat()); + await this.manager?.getSource().invalidateContent(visibleDay, !afterChange); - await Promise.all(promises).finally(() => - this.fetchData(sync)); - } - - /** - * Load categories to be able to filter events. - * - * @return Promise resolved when done. - */ - protected async loadCategories(): Promise { - try { - const cats = await CoreCourses.getCategories(0, true); - this.categories = {}; - - // Index categories by ID. - cats.forEach((category) => { - this.categories[category.id] = category; - }); - } catch { - // Ignore errors. - } + await this.fetchData(sync); } /** @@ -501,11 +325,16 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { if (result.updated) { // Trigger a manual sync event. + const visibleDay = this.slides?.getCurrentItem(); result.source = 'day'; - result.day = this.day; - result.month = this.month; - result.year = this.year; + if (visibleDay) { + result.day = visibleDay.dayNumber; + result.month = visibleDay.monthNumber; + result.year = visibleDay.year; + } + + this.manager?.getSource().markAllItemsUnloaded(); CoreEvents.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId); } } catch (error) { @@ -533,7 +362,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { await CoreDomUtils.openPopover({ component: AddonCalendarFilterPopoverComponent, componentProps: { - courses: this.courses, + courses: this.manager?.getSource().courses, filter: this.filter, }, event, @@ -551,7 +380,12 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { if (!eventId) { // It's a new event, set the time. eventId = 0; - params.timestamp = moment().year(this.year).month(this.month - 1).date(this.day).unix() * 1000; + + const visibleDay = this.slides?.getCurrentItem(); + if (visibleDay) { + params.timestamp = moment().year(visibleDay.year).month(visibleDay.monthNumber - 1).date(visibleDay.dayNumber) + .unix() * 1000; + } } if (this.filter.courseId) { @@ -561,141 +395,76 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { CoreNavigator.navigateToSitePath(`/calendar/edit/${eventId}`, { params }); } - /** - * Calculate current moment. - */ - calculateCurrentMoment(): void { - this.currentMoment = moment().year(this.year).month(this.month - 1).date(this.day); - } - /** * Check if user is viewing the current day. */ calculateIsCurrentDay(): void { - const now = new Date(); + const visibleDay = this.slides?.getCurrentItem(); + if (!visibleDay) { + return; + } - this.currentTime = CoreTimeUtils.timestamp(); + this.isCurrentDay = visibleDay.isCurrentDay; + } - this.isCurrentDay = this.year == now.getFullYear() && this.month == now.getMonth() + 1 && this.day == now.getDate(); - this.isPastDay = this.year < now.getFullYear() || (this.year == now.getFullYear() && this.month < now.getMonth()) || - (this.year == now.getFullYear() && this.month == now.getMonth() + 1 && this.day < now.getDate()); + /** + * Check whether visible day has offline data. + * + * @return Whether visible day has offline data. + */ + visibleDayHasOffline(): boolean { + const visibleDay = this.slides?.getCurrentItem(); + + return !!visibleDay && visibleDay.hasOffline; } /** * Go to current day. */ async goToCurrentDay(): Promise { - const now = new Date(); - const initialDay = this.day; - const initialMonth = this.month; - const initialYear = this.year; - - this.day = now.getDate(); - this.month = now.getMonth() + 1; - this.year = now.getFullYear(); - this.calculateCurrentMoment(); + const manager = this.manager; + const slides = this.slides; + if (!manager || !slides) { + return; + } + const currentDay = CoreTimeUtils.getCurrentDay(); this.loaded = false; try { - await this.fetchEvents(); + // Make sure the day is loaded. + await manager.getSource().loadItem(currentDay); + slides.slideToItem(currentDay); this.isCurrentDay = true; } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); - - this.year = initialYear; - this.month = initialMonth; - this.day = initialDay; - this.calculateCurrentMoment(); + } finally { + this.loaded = true; } - - this.loaded = true; } /** * Load next day. */ async loadNext(): Promise { - this.increaseDay(); - - this.loaded = false; - - try { - await this.fetchEvents(); - } catch (error) { - CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); - this.decreaseDay(); - } - this.loaded = true; + this.slides?.slideNext(); } /** * Load previous day. */ async loadPrevious(): Promise { - this.decreaseDay(); - - this.loaded = false; - - try { - await this.fetchEvents(); - } catch (error) { - CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); - this.increaseDay(); - } - this.loaded = true; + this.slides?.slidePrev(); } /** - * Decrease the current day. - */ - protected decreaseDay(): void { - this.currentMoment.subtract(1, 'day'); - - this.year = this.currentMoment.year(); - this.month = this.currentMoment.month() + 1; - this.day = this.currentMoment.date(); - } - - /** - * Increase the current day. - */ - protected increaseDay(): void { - this.currentMoment.add(1, 'day'); - - this.year = this.currentMoment.year(); - this.month = this.currentMoment.month() + 1; - this.day = this.currentMoment.date(); - } - - /** - * Find an event and mark it as deleted. + * Slide has changed. * - * @param eventId Event ID. - * @param deleted Whether to mark it as deleted or not. - * @return Whether the event was found. + * @param data Data about new item. */ - protected markAsDeleted(eventId: number, deleted: boolean): boolean { - const event = this.onlineEvents.find((event) => event.id == eventId); - - if (event) { - event.deleted = deleted; - - return true; - } - - return false; - } - - /** - * Returns if the event is in the past or not. - * - * @param event Event object. - * @return True if it's in the past. - */ - isEventPast(event: AddonCalendarEventToDisplay): boolean { - return (event.timestart + event.timeduration) < this.currentTime; + slideChanged(data: CoreSwipeCurrentItemData): void { + this.viewDay(data.item); } /** @@ -715,3 +484,359 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { } } + +/** + * Preloaded month. + */ +type PreloadedDay = YearMonthDay & CoreSwipeSlidesDynamicItem & { + events: AddonCalendarEventToDisplay[]; // Events (both online and offline). + onlineEvents: AddonCalendarEventToDisplay[]; + filteredEvents: AddonCalendarEventToDisplay[]; + isCurrentDay: boolean; + isPastDay: boolean; + hasOffline: boolean; // Whether the day has offline data. +}; + +/** + * Helper to manage swiping within days. + */ +class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicItemsManagerSource { + + courses: Partial[] = []; + // Offline events classified in month & day. + offlineEvents: Record> = {}; + offlineEditedEventsIds: number[] = []; // IDs of events edited in offline. + categories: { [id: number]: CoreCategoryData } = {}; + deletedEvents: number[] = []; // Events deleted in offline. + timeFormat?: string; + canCreate = false; + + protected dayPage: AddonCalendarDayPage; + protected categoriesRetrieved = false; + + constructor(page: AddonCalendarDayPage, initialDay: YearMonthDay) { + super(initialDay); + + this.dayPage = page; + } + + /** + * Fetch data. + * + * @param courseId Current selected course id (if any). + * @return Promise resolved when done. + */ + async fetchData(courseId?: number): Promise { + await Promise.all([ + this.loadCourses(courseId), + this.loadCanCreate(courseId), + this.loadCategories(), + this.loadOfflineEvents(), + this.loadOfflineDeletedEvents(), + this.loadTimeFormat(), + ]); + } + + /** + * Filter all loaded days events based on the filter popover. + * + * @param filter Filter to apply. + */ + filterAllDayEvents(filter: AddonCalendarFilter): void { + this.getItems()?.forEach(day => this.filterEvents(day, filter)); + } + + /** + * Filter events of a certain day based on the filter popover. + * + * @param day Day with the events. + * @param filter Filter to apply. + */ + filterEvents(day: PreloadedDay, filter: AddonCalendarFilter): void { + day.filteredEvents = AddonCalendarHelper.getFilteredEvents(day.events, filter, this.categories); + } + + /** + * Load courses. + * + * @param courseId Current selected course id (if any). + * @return Promise resolved when done. + */ + async loadCourses(courseId?: number): Promise { + const data = await CoreCoursesHelper.getCoursesForPopover(courseId); + + this.courses = data.courses; + } + + /** + * Load whether user can create events. + * + * @param courseId Current selected course id (if any). + * @return Promise resolved when done. + */ + async loadCanCreate(courseId?: number): Promise { + this.canCreate = await AddonCalendarHelper.canEditEvents(courseId); + } + + /** + * Load categories to be able to filter events. + * + * @return Promise resolved when done. + */ + async loadCategories(): Promise { + if (this.categoriesRetrieved) { + // Already retrieved, stop. + return; + } + + try { + const cats = await CoreCourses.getCategories(0, true); + this.categoriesRetrieved = true; + this.categories = {}; + + // Index categories by ID. + cats.forEach((category) => { + this.categories[category.id] = category; + }); + } catch { + // Ignore errors. + } + } + + /** + * Load events created or edited in offline. + * + * @return Promise resolved when done. + */ + async loadOfflineEvents(): Promise { + // Get offline events. + const events = await AddonCalendarOffline.getAllEditedEvents(); + + // Classify them by month & day. + this.offlineEvents = AddonCalendarHelper.classifyIntoMonths(events); + + // Get the IDs of events edited in offline. + this.offlineEditedEventsIds = events.filter((event) => event.id > 0).map((event) => event.id); + } + + /** + * Load events deleted in offline. + * + * @return Promise resolved when done. + */ + async loadOfflineDeletedEvents(): Promise { + this.deletedEvents = await AddonCalendarOffline.getAllDeletedEventsIds(); + } + + /** + * Load time format. + * + * @return Promise resolved when done. + */ + async loadTimeFormat(): Promise { + this.timeFormat = await AddonCalendar.getCalendarTimeFormat(); + } + + /** + * @inheritdoc + */ + getItemId(item: YearMonthDay): string | number { + return AddonCalendarHelper.getDayId(item); + } + + /** + * @inheritdoc + */ + getPreviousItem(item: YearMonthDay): YearMonthDay | null { + return CoreTimeUtils.getPreviousDay(item); + } + + /** + * @inheritdoc + */ + getNextItem(item: YearMonthDay): YearMonthDay | null { + return CoreTimeUtils.getNextDay(item); + } + + /** + * @inheritdoc + */ + async loadItemData(day: YearMonthDay, preload = false): Promise { + const preloadedDay: PreloadedDay = { + ...day, + hasOffline: false, + events: [], + onlineEvents: [], + filteredEvents: [], + isCurrentDay: CoreTimeUtils.isSameDay(day, CoreTimeUtils.getCurrentDay()), + isPastDay: CoreTimeUtils.isPastDay(day), + }; + + if (preload) { + return preloadedDay; + } + + let result: AddonCalendarCalendarDay; + + try { + // Don't pass courseId and categoryId, we'll filter them locally. + result = await AddonCalendar.getDayEvents(day.year, day.monthNumber, day.dayNumber); + preloadedDay.onlineEvents = await Promise.all( + result.events.map((event) => AddonCalendarHelper.formatEventData(event)), + ); + } catch (error) { + // Allow navigating to non-cached days in offline (behave as if using emergency cache). + if (CoreApp.isOnline()) { + throw error; + } + } + + // Schedule notifications for the events retrieved (only future events will be scheduled). + AddonCalendar.scheduleEventsNotifications(preloadedDay.onlineEvents); + + // Merge the online events with offline data. + preloadedDay.events = this.mergeEvents(preloadedDay); + + // Filter events by course. + this.filterEvents(preloadedDay, this.dayPage.filter); + + // Re-calculate the formatted time so it uses the device date. + const dayTime = moment().year(day.year).month(day.monthNumber - 1).date(day.dayNumber).unix() * 1000; + const currentTime = CoreTimeUtils.timestamp(); + + const promises = preloadedDay.events.map(async (event) => { + event.ispast = preloadedDay.isPastDay || (preloadedDay.isCurrentDay && this.isEventPast(event, currentTime)); + event.formattedtime = await AddonCalendar.formatEventTime(event, this.dayPage.timeFormat, true, dayTime); + }); + + await Promise.all(promises); + + return preloadedDay; + } + + /** + * Returns if the event is in the past or not. + * + * @param event Event object. + * @param currentTime Current time. + * @return True if it's in the past. + */ + isEventPast(event: AddonCalendarEventToDisplay, currentTime: number): boolean { + return (event.timestart + event.timeduration) < currentTime; + } + + /** + * Merge online events with the offline events of that period. + * + * @param day Day with the events. + * @return Merged events. + */ + mergeEvents(day: PreloadedDay): AddonCalendarEventToDisplay[] { + day.hasOffline = false; + + if (!Object.keys(this.offlineEvents).length && !this.deletedEvents.length) { + // No offline events, nothing to merge. + return day.onlineEvents; + } + + const monthOfflineEvents = this.offlineEvents[AddonCalendarHelper.getMonthId(day)]; + const dayOfflineEvents = monthOfflineEvents && monthOfflineEvents[day.dayNumber]; + let result = day.onlineEvents; + + if (this.deletedEvents.length) { + // Mark as deleted the events that were deleted in offline. + result.forEach((event) => { + event.deleted = this.deletedEvents.indexOf(event.id) != -1; + + if (event.deleted) { + day.hasOffline = true; + } + }); + } + + if (this.offlineEditedEventsIds.length) { + // Remove the online events that were modified in offline. + result = result.filter((event) => this.offlineEditedEventsIds.indexOf(event.id) == -1); + + if (result.length != day.onlineEvents.length) { + day.hasOffline = true; + } + } + + if (dayOfflineEvents && dayOfflineEvents.length) { + // Add the offline events (either new or edited). + day.hasOffline = true; + result = AddonCalendarHelper.sortEvents(result.concat(dayOfflineEvents)); + } + + return result; + } + + /** + * Invalidate content. + * + * @param visibleDay The current visible day. + * @param invalidateDayEvents Whether to invalidate visible day events. + * @return Promise resolved when done. + */ + async invalidateContent(visibleDay: PreloadedDay | null, invalidateDayEvents?: boolean): Promise { + const promises: Promise[] = []; + + if (invalidateDayEvents && visibleDay) { + promises.push(AddonCalendar.invalidateDayEvents(visibleDay.year, visibleDay.monthNumber, visibleDay.dayNumber)); + } + promises.push(AddonCalendar.invalidateAllowedEventTypes()); + promises.push(CoreCourses.invalidateCategories(0, true)); + promises.push(AddonCalendar.invalidateTimeFormat()); + + this.categoriesRetrieved = false; // Get categories again. + + if (visibleDay) { + visibleDay.dirty = true; + } + + await Promise.all(promises); + } + + /** + * Find an event and mark it as deleted. + * + * @param eventId Event ID. + * @param deleted Whether to mark it as deleted or not. + */ + markAsDeleted(eventId: number, deleted: boolean): void { + // Mark the event as deleted or not. + this.getItems()?.some(day => { + const event = day.onlineEvents.find((event) => event.id == eventId); + + if (!event) { + return false; + } + + event.deleted = deleted; + + if (deleted) { + day.hasOffline = true; + } else { + // Re-calculate "hasOffline". + day.hasOffline = day.events.length != day.onlineEvents.length || + day.events.some((event) => event.deleted || event.offline); + } + + return true; + }); + + // Add it or remove it from the list of deleted events. + if (deleted) { + if (!this.deletedEvents.includes(eventId)) { + this.deletedEvents.push(eventId); + } + } else { + const index = this.deletedEvents.indexOf(eventId); + if (index != -1) { + this.deletedEvents.splice(index, 1); + } + } + } + +} diff --git a/src/addons/calendar/pages/day/day.scss b/src/addons/calendar/pages/day/day.scss index a2b9031b0..d3fdad33c 100644 --- a/src/addons/calendar/pages/day/day.scss +++ b/src/addons/calendar/pages/day/day.scss @@ -6,4 +6,8 @@ font-size: 1.2rem; } } + + core-swipe-slides { + --swipe-slides-min-height: calc(100% - 52px); + } } diff --git a/src/addons/calendar/pages/index/index.page.ts b/src/addons/calendar/pages/index/index.page.ts index f2c8fb51b..ba5d5366d 100644 --- a/src/addons/calendar/pages/index/index.page.ts +++ b/src/addons/calendar/pages/index/index.page.ts @@ -92,7 +92,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { (data) => { if (data && data.eventId) { this.loaded = false; - this.refreshData(true, false); + this.refreshData(true, false, true); } }, this.currentSiteId, @@ -101,7 +101,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { // Listen for new event discarded event. When it does, reload the data. this.discardedObserver = CoreEvents.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => { this.loaded = false; - this.refreshData(true, false); + this.refreshData(true, false, true); }, this.currentSiteId); // Listen for events edited. When an event is edited, reload the data. @@ -110,7 +110,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { (data) => { if (data && data.eventId) { this.loaded = false; - this.refreshData(true, false); + this.refreshData(true, false, true); } }, this.currentSiteId, @@ -119,21 +119,21 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { // Refresh data if calendar events are synchronized automatically. this.syncObserver = CoreEvents.on(AddonCalendarSyncProvider.AUTO_SYNCED, () => { this.loaded = false; - this.refreshData(false, false); + this.refreshData(false, false, true); }, this.currentSiteId); // Refresh data if calendar events are synchronized manually but not by this page. this.manualSyncObserver = CoreEvents.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => { if (data && data.source != 'index') { this.loaded = false; - this.refreshData(false, false); + this.refreshData(false, false, true); } }, this.currentSiteId); // Update the events when an event is deleted. this.deleteEventObserver = CoreEvents.on(AddonCalendarProvider.DELETED_EVENT_EVENT, () => { this.loaded = false; - this.refreshData(false, false); + this.refreshData(false, false, true); }, this.currentSiteId); // Update the "hasOffline" property if an event deleted in offline is restored. @@ -278,7 +278,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { * @param afterChange Whether the refresh is done after an event has changed or has been synced. * @return Promise resolved when done. */ - async refreshData(sync = false, showErrors = false): Promise { + async refreshData(sync = false, showErrors = false, afterChange = false): Promise { this.syncIcon = CoreConstants.ICON_LOADING; const promises: Promise[] = []; @@ -287,7 +287,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { // Refresh the sub-component. if (this.showCalendar && this.calendarComponent) { - promises.push(this.calendarComponent.refreshData()); + promises.push(this.calendarComponent.refreshData(afterChange)); } else if (!this.showCalendar && this.upcomingEventsComponent) { promises.push(this.upcomingEventsComponent.refreshData()); } diff --git a/src/addons/calendar/services/calendar-helper.ts b/src/addons/calendar/services/calendar-helper.ts index d441b1c1d..74f588a7b 100644 --- a/src/addons/calendar/services/calendar-helper.ts +++ b/src/addons/calendar/services/calendar-helper.ts @@ -37,6 +37,7 @@ import { AddonCalendarSyncInvalidateEvent } from './calendar-sync'; import { AddonCalendarOfflineEventDBRecord } from './database/calendar-offline'; import { CoreCategoryData } from '@features/courses/services/courses'; import { AddonCalendarReminderDBRecord } from './database/calendar'; +import { YearAndMonth, YearMonthDay } from '@services/utils/time'; /** * Context levels enumeration. @@ -140,7 +141,7 @@ export class AddonCalendarHelperProvider { // Add the event to all the days it lasts. while (!treatedDay.isAfter(endDay, 'day')) { - const monthId = this.getMonthId(treatedDay.year(), treatedDay.month() + 1); + const monthId = this.getMonthId({ year: treatedDay.year(), monthNumber: treatedDay.month() + 1 }); const day = treatedDay.date(); if (!result[monthId]) { @@ -364,14 +365,23 @@ export class AddonCalendarHelperProvider { } /** - * Get the month "id" (year + month). + * Get the month "id". * - * @param year Year. - * @param month Month. + * @param month Month data. * @return The "id". */ - getMonthId(year: number, month: number): string { - return year + '#' + month; + getMonthId(month: YearAndMonth): string { + return `${month.year}#${month.monthNumber}`; + } + + /** + * Get the day "id". + * + * @param day Day data. + * @return The "id". + */ + getDayId(day: YearMonthDay): string { + return `${day.year}#${day.monthNumber}#${day.dayNumber}`; } /** @@ -650,7 +660,7 @@ export class AddonCalendarHelperProvider { fetchTimestarts.map((fetchTime) => { const day = moment(new Date(fetchTime * 1000)); - const monthId = this.getMonthId(day.year(), day.month() + 1); + const monthId = this.getMonthId({ year: day.year(), monthNumber: day.month() + 1 }); if (!treatedMonths[monthId]) { // Month not refetch or invalidated already, do it now. treatedMonths[monthId] = true; @@ -686,7 +696,7 @@ export class AddonCalendarHelperProvider { invalidateTimestarts.map((fetchTime) => { const day = moment(new Date(fetchTime * 1000)); - const monthId = this.getMonthId(day.year(), day.month() + 1); + const monthId = this.getMonthId({ year: day.year(), monthNumber: day.month() + 1 }); if (!treatedMonths[monthId]) { // Month not refetch or invalidated already, do it now. treatedMonths[monthId] = true; diff --git a/src/core/classes/items-management/slides-dynamic-items-manager-source.ts b/src/core/classes/items-management/slides-dynamic-items-manager-source.ts index 9422f7514..a7324ebbc 100644 --- a/src/core/classes/items-management/slides-dynamic-items-manager-source.ts +++ b/src/core/classes/items-management/slides-dynamic-items-manager-source.ts @@ -17,15 +17,22 @@ import { CoreSwipeSlidesItemsManagerSource } from './slides-items-manager-source /** * Items collection source data for "swipe slides". */ -export abstract class CoreSwipeSlidesDynamicItemsManagerSource extends CoreSwipeSlidesItemsManagerSource { +export abstract class CoreSwipeSlidesDynamicItemsManagerSource + extends CoreSwipeSlidesItemsManagerSource { + + // Items being loaded, to prevent loading them twice. + protected loadingItems: Record = {}; /** * @inheritdoc */ - async load(): Promise { - if (this.initialItem) { + async load(currentItem?: Partial | null): Promise { + if (!this.initialized && this.initialItem) { // Load the initial item. await this.loadItem(this.initialItem); + } else if (this.initialized && currentItem) { + // Reload current item if needed. + await this.loadItem(currentItem); } this.setInitialized(); @@ -64,7 +71,7 @@ export abstract class CoreSwipeSlidesDynamicItemsManagerSource e * @return Promise resolved when done. */ async loadItemInList(item: Partial, preload = false): Promise { - const preloadedItem = await this.loadItemData(item, preload); + const preloadedItem = await this.performLoadItemData(item, preload); if (!preloadedItem) { return; } @@ -113,6 +120,70 @@ export abstract class CoreSwipeSlidesDynamicItemsManagerSource e this.notifyItemsUpdated(); } + /** + * Load or preload a certain item data. + * This helper function will check some common cases so they don't have to be replicated in all loadItemData implementations. + * + * @param item Basic data about the item to load. + * @param preload Whether to preload. + * @return Promise resolved with item. Resolve with null if already loading or item is not valid (e.g. there are no more items). + */ + protected async performLoadItemData(item: Partial, preload: boolean): Promise { + const itemId = this.getItemId(item); + + if (!itemId || this.loadingItems[itemId]) { + // Not valid or already loading, ignore it. + return null; + } + + const existingItem = this.getItem(item); + if (existingItem && ((existingItem.loaded && !existingItem.dirty) || preload)) { + // Already loaded, or preloading an already preloaded item. + return existingItem; + } + + // Load the item. + this.loadingItems[itemId] = true; + + try { + const itemData = await this.loadItemData(item, preload); + + if (itemData && !preload) { + itemData.loaded = true; + itemData.dirty = false; + } + + if (existingItem && itemData) { + // Update item that is already in list. + Object.assign(existingItem, itemData); + + return existingItem; + } + + return itemData; + } finally { + this.loadingItems[itemId] = false; + } + } + + /** + * Mark all items as dirty. + */ + markAllItemsDirty(): void { + this.getItems()?.forEach(item => { + item.dirty = true; + }); + } + + /** + * Mark all items as not loaded. + */ + markAllItemsUnloaded(): void { + this.getItems()?.forEach(item => { + item.loaded = false; + }); + } + /** * Load or preload a certain item data. * @@ -139,3 +210,8 @@ export abstract class CoreSwipeSlidesDynamicItemsManagerSource e abstract getNextItem(item: Partial): Partial | null; } + +export type CoreSwipeSlidesDynamicItem = { + loaded?: boolean; // Whether the item has been loaded. This value can affect UI (e.g. to display a spinner). + dirty?: boolean; // Whether the item data needs to be reloaded. This value usually shouldn't affect UI. +}; diff --git a/src/core/classes/items-management/slides-dynamic-items-manager.ts b/src/core/classes/items-management/slides-dynamic-items-manager.ts index cacae1282..718e71389 100644 --- a/src/core/classes/items-management/slides-dynamic-items-manager.ts +++ b/src/core/classes/items-management/slides-dynamic-items-manager.ts @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreSwipeSlidesDynamicItemsManagerSource } from './slides-dynamic-items-manager-source'; +import { CoreSwipeSlidesDynamicItem, CoreSwipeSlidesDynamicItemsManagerSource } from './slides-dynamic-items-manager-source'; import { CoreSwipeSlidesItemsManager } from './slides-items-manager'; /** * Helper class to manage items for core-swipe-slides. */ export class CoreSwipeSlidesDynamicItemsManager< - Item = unknown, + Item extends CoreSwipeSlidesDynamicItem, Source extends CoreSwipeSlidesDynamicItemsManagerSource = CoreSwipeSlidesDynamicItemsManagerSource, > extends CoreSwipeSlidesItemsManager { diff --git a/src/core/components/swipe-slides/swipe-slides.scss b/src/core/components/swipe-slides/swipe-slides.scss index 6b0b7043d..71cf2f9f6 100644 --- a/src/core/components/swipe-slides/swipe-slides.scss +++ b/src/core/components/swipe-slides/swipe-slides.scss @@ -1,4 +1,10 @@ :host { + --swipe-slides-min-height: auto; + + ion-slides { + min-height: var(--swipe-slides-min-height); + } + ion-slide { display: block; font-size: inherit; diff --git a/src/core/services/utils/time.ts b/src/core/services/utils/time.ts index cd9c449f3..5c8e420de 100644 --- a/src/core/services/utils/time.ts +++ b/src/core/services/utils/time.ts @@ -396,16 +396,12 @@ export class CoreTimeUtilsProvider { * @return Previous month and its year. */ getPreviousMonth(month: YearAndMonth): YearAndMonth { - if (month.monthNumber === 1) { - return { - monthNumber: 12, - year: month.year - 1, - }; - } + const dayMoment = moment().year(month.year).month(month.monthNumber - 1); + dayMoment.subtract(1, 'month'); return { - monthNumber: month.monthNumber - 1, - year: month.year, + year: dayMoment.year(), + monthNumber: dayMoment.month() + 1, }; } @@ -416,29 +412,27 @@ export class CoreTimeUtilsProvider { * @return Next month and its year. */ getNextMonth(month: YearAndMonth): YearAndMonth { - if (month.monthNumber === 12) { - return { - monthNumber: 1, - year: month.year + 1, - }; - } + const dayMoment = moment().year(month.year).month(month.monthNumber - 1); + dayMoment.add(1, 'month'); return { - monthNumber: month.monthNumber + 1, - year: month.year, + year: dayMoment.year(), + monthNumber: dayMoment.month() + 1, }; } /** - * Check if a certain month is current month. + * Get current month. * - * @param month Year and month. - * @return Whether it's current month. + * @return Current month. */ - isCurrentMonth(month: YearAndMonth): boolean { + getCurrentMonth(): YearAndMonth { const now = new Date(); - return month.year == now.getFullYear() && month.monthNumber == now.getMonth() + 1; + return { + year: now.getFullYear(), + monthNumber: now.getMonth() + 1, + }; } /** @@ -454,8 +448,6 @@ export class CoreTimeUtilsProvider { } /** - * Check if two months are the same. - * * @param monthA Month A. * @param monthB Month B. * @return Whether it's same month. @@ -464,11 +456,94 @@ export class CoreTimeUtilsProvider { return monthA.monthNumber === monthB.monthNumber && monthA.year === monthB.year; } + /** + * Given a day data, return the previous day. + * + * @param day Day. + * @return Previous day. + */ + getPreviousDay(day: YearMonthDay): YearMonthDay { + const dayMoment = moment().year(day.year).month(day.monthNumber - 1).date(day.dayNumber); + dayMoment.subtract(1, 'day'); + + return { + year: dayMoment.year(), + monthNumber: dayMoment.month() + 1, + dayNumber: dayMoment.date(), + }; + } + + /** + * Given a day data, return the next day. + * + * @param day Day. + * @return Next day. + */ + getNextDay(day: YearMonthDay): YearMonthDay { + const dayMoment = moment().year(day.year).month(day.monthNumber - 1).date(day.dayNumber); + dayMoment.add(1, 'day'); + + return { + year: dayMoment.year(), + monthNumber: dayMoment.month() + 1, + dayNumber: dayMoment.date(), + }; + } + + /** + * Get current day. + * + * @return Current day. + */ + getCurrentDay(): YearMonthDay { + const now = new Date(); + + return { + year: now.getFullYear(), + monthNumber: now.getMonth() + 1, + dayNumber: now.getDate(), + }; + } + + /** + * Check if a certain day is a past day. + * + * @param day Day. + * @return Whether it's a past day. + */ + isPastDay(day: YearMonthDay): boolean { + const now = new Date(); + + return day.year < now.getFullYear() || (day.year === now.getFullYear() && day.monthNumber < now.getMonth() + 1) || + (day.year === now.getFullYear() && day.monthNumber === now.getMonth() + 1 && day.dayNumber < now.getDate()); + } + + /** + * Check if two days are the same. + * + * @param dayA Day A. + * @param dayB Day B. + * @return Whether it's same day. + */ + isSameDay(dayA: YearMonthDay, dayB: YearMonthDay): boolean { + return dayA.dayNumber === dayB.dayNumber && dayA.monthNumber === dayB.monthNumber && dayA.year === dayB.year; + } + } export const CoreTimeUtils = makeSingleton(CoreTimeUtilsProvider); +/** + * Data to identify a month. + */ export type YearAndMonth = { year: number; // Year number. monthNumber: number; // Month number (1 to 12). }; + +/** + * Data to identify a day. + */ +export type YearMonthDay = YearAndMonth & { + dayNumber: number; // Day number. +};