From af461e7dacf7d1807c0782581381002f7189dbb1 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 29 Aug 2019 13:42:04 +0200 Subject: [PATCH] MOBILE-3068 calendar: Autofetch day&month when event changed --- .../calendar/components/calendar/calendar.ts | 10 ++- .../upcoming-events/upcoming-events.ts | 10 ++- src/addon/calendar/pages/day/day.ts | 20 +++-- .../calendar/pages/edit-event/edit-event.ts | 2 +- src/addon/calendar/pages/event/event.ts | 4 +- src/addon/calendar/pages/index/index.ts | 19 ++--- src/addon/calendar/providers/calendar-sync.ts | 2 +- src/addon/calendar/providers/calendar.ts | 37 ++++++--- src/addon/calendar/providers/helper.ts | 78 ++++++++++++------- 9 files changed, 118 insertions(+), 64 deletions(-) diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 7b088b76f..269310608 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -285,14 +285,16 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest /** * Refresh events. * - * @param {boolean} [sync] Whether it should try to synchronize offline events. - * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @param {boolean} [afterChange] Whether the refresh is done after an event has changed or has been synced. * @return {Promise} Promise resolved when done. */ - refreshData(sync?: boolean, showErrors?: boolean): Promise { + refreshData(afterChange?: boolean): Promise { const promises = []; - promises.push(this.calendarProvider.invalidateMonthlyEvents(this.year, this.month)); + // Don't invalidate monthly events after a change, it has already been handled. + if (!afterChange) { + promises.push(this.calendarProvider.invalidateMonthlyEvents(this.year, this.month)); + } promises.push(this.coursesProvider.invalidateCategories(0, true)); promises.push(this.calendarProvider.invalidateTimeFormat()); diff --git a/src/addon/calendar/components/upcoming-events/upcoming-events.ts b/src/addon/calendar/components/upcoming-events/upcoming-events.ts index 74db1aebc..d56a3470b 100644 --- a/src/addon/calendar/components/upcoming-events/upcoming-events.ts +++ b/src/addon/calendar/components/upcoming-events/upcoming-events.ts @@ -225,14 +225,16 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, /** * Refresh events. * - * @param {boolean} [sync] Whether it should try to synchronize offline events. - * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @param {boolean} [afterChange] Whether the refresh is done after an event has changed or has been synced. * @return {Promise} Promise resolved when done. */ - refreshData(sync?: boolean, showErrors?: boolean): Promise { + refreshData(afterChange?: boolean): Promise { const promises = []; - promises.push(this.calendarProvider.invalidateAllUpcomingEvents()); + // Don't invalidate upcoming events after a change, it has already been handled. + if (!afterChange) { + promises.push(this.calendarProvider.invalidateAllUpcomingEvents()); + } promises.push(this.coursesProvider.invalidateCategories(0, true)); promises.push(this.calendarProvider.invalidateLookAhead()); promises.push(this.calendarProvider.invalidateTimeFormat()); diff --git a/src/addon/calendar/pages/day/day.ts b/src/addon/calendar/pages/day/day.ts index cc930e728..c3b566f88 100644 --- a/src/addon/calendar/pages/day/day.ts +++ b/src/addon/calendar/pages/day/day.ts @@ -113,35 +113,35 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { if (data && data.event) { this.loaded = false; - this.refreshData(true, false); + this.refreshData(true, false, true); } }, this.currentSiteId); // Listen for new event discarded event. When it does, reload the data. this.discardedObserver = eventsProvider.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. this.editEventObserver = eventsProvider.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => { if (data && data.event) { this.loaded = false; - this.refreshData(true, false); + this.refreshData(true, false, true); } }, this.currentSiteId); // Refresh data if calendar events are synchronized automatically. this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => { this.loaded = false; - this.refreshData(); + this.refreshData(false, false, true); }, this.currentSiteId); // Refresh data if calendar events are synchronized manually but not by this page. this.manualSyncObserver = eventsProvider.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; - this.refreshData(); + this.refreshData(false, false, true); } }, this.currentSiteId); @@ -153,7 +153,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.deletedEvents.push(data.eventId); } else { this.loaded = false; - this.refreshData(); + this.refreshData(false, false, true); } }, this.currentSiteId); @@ -425,15 +425,19 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { * * @param {boolean} [sync] Whether it should try to synchronize offline events. * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @param {boolean} [afterChange] Whether the refresh is done after an event has changed or has been synced. * @return {Promise} Promise resolved when done. */ - refreshData(sync?: boolean, showErrors?: boolean): Promise { + refreshData(sync?: boolean, showErrors?: boolean, afterChange?: boolean): Promise { this.syncIcon = 'spinner'; const promises = []; + // Don't invalidate day events after a change, it has already been handled. + if (!afterChange) { + promises.push(this.calendarProvider.invalidateDayEvents(this.year, this.month, this.day)); + } promises.push(this.calendarProvider.invalidateAllowedEventTypes()); - promises.push(this.calendarProvider.invalidateDayEvents(this.year, this.month, this.day)); promises.push(this.coursesProvider.invalidateCategories(0, true)); promises.push(this.calendarProvider.invalidateTimeFormat()); diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index 4cc40a194..b8d8ef9f4 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -499,7 +499,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { const numberOfRepetitions = formData.repeat ? formData.repeats : (data.repeateditall && this.event.othereventscount ? this.event.othereventscount + 1 : 1); - this.calendarHelper.invalidateRepeatedEventsOnCalendarForEvent(result.event, numberOfRepetitions).catch(() => { + return this.calendarHelper.refreshAfterChangeEvent(result.event, numberOfRepetitions).catch(() => { // Ignore errors. }); } diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index d23579df0..8edc9b96e 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -449,8 +449,8 @@ export class AddonCalendarEventPage implements OnDestroy { if (sent) { // Event deleted, invalidate right days & months. - promise = this.calendarHelper.invalidateRepeatedEventsOnCalendarForEvent(this.event, - deleteAll ? this.event.eventcount : 1).catch(() => { + promise = this.calendarHelper.refreshAfterChangeEvent(this.event, deleteAll ? this.event.eventcount : 1) + .catch(() => { // Ignore errors. }); } else { diff --git a/src/addon/calendar/pages/index/index.ts b/src/addon/calendar/pages/index/index.ts index 341cf36b3..8f3d5a129 100644 --- a/src/addon/calendar/pages/index/index.ts +++ b/src/addon/calendar/pages/index/index.ts @@ -95,42 +95,42 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { if (data && data.event) { this.loaded = false; - this.refreshData(true, false); + this.refreshData(true, false, true); } }, this.currentSiteId); // Listen for new event discarded event. When it does, reload the data. this.discardedObserver = eventsProvider.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. this.editEventObserver = eventsProvider.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => { if (data && data.event) { this.loaded = false; - this.refreshData(true, false); + this.refreshData(true, false, true); } }, this.currentSiteId); // Refresh data if calendar events are synchronized automatically. this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => { this.loaded = false; - this.refreshData(); + this.refreshData(false, false, true); }, this.currentSiteId); // Refresh data if calendar events are synchronized manually but not by this page. this.manualSyncObserver = eventsProvider.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => { if (data && data.source != 'index') { this.loaded = false; - this.refreshData(); + this.refreshData(false, false, true); } }, this.currentSiteId); // Update the events when an event is deleted. this.deleteEventObserver = eventsProvider.on(AddonCalendarProvider.DELETED_EVENT_EVENT, (data) => { this.loaded = false; - this.refreshData(); + this.refreshData(false, false, true); }, this.currentSiteId); // Update the "hasOffline" property if an event deleted in offline is restored. @@ -251,9 +251,10 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { * * @param {boolean} [sync] Whether it should try to synchronize offline events. * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @param {boolean} [afterChange] Whether the refresh is done after an event has changed or has been synced. * @return {Promise} Promise resolved when done. */ - refreshData(sync?: boolean, showErrors?: boolean): Promise { + refreshData(sync?: boolean, showErrors?: boolean, afterChange?: boolean): Promise { this.syncIcon = 'spinner'; const promises = []; @@ -262,9 +263,9 @@ 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()); + promises.push(this.upcomingEventsComponent.refreshData(afterChange)); } return Promise.all(promises).finally(() => { diff --git a/src/addon/calendar/providers/calendar-sync.ts b/src/addon/calendar/providers/calendar-sync.ts index b395ae6af..bc9d96f33 100644 --- a/src/addon/calendar/providers/calendar-sync.ts +++ b/src/addon/calendar/providers/calendar-sync.ts @@ -159,7 +159,7 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { // Data has been sent to server. Now invalidate the WS calls. const promises = [ this.calendarProvider.invalidateEventsList(siteId), - this.calendarHelper.invalidateRepeatedEventsOnCalendar(result.toinvalidate, siteId) + this.calendarHelper.refreshAfterChangeEvents(result.toinvalidate, siteId) ]; return Promise.all(promises).catch(() => { diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 17f372590..892cb35cd 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; -import { CoreSite } from '@classes/site'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreAppProvider } from '@providers/app'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -971,10 +971,12 @@ export class AddonCalendarProvider { * @param {number} day Day to get. * @param {number} [courseId] Course to get. * @param {number} [categoryId] Category to get. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the response. */ - getDayEvents(year: number, month: number, day: number, courseId?: number, categoryId?: number, siteId?: string): Promise { + getDayEvents(year: number, month: number, day: number, courseId?: number, categoryId?: number, ignoreCache?: boolean, + siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { @@ -991,11 +993,16 @@ export class AddonCalendarProvider { data.categoryid = categoryId; } - const preSets = { + const preSets: CoreSiteWSPreSets = { cacheKey: this.getDayEventsCacheKey(year, month, day, courseId, categoryId), updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('core_calendar_get_calendar_day_view', data, preSets).then((response) => { this.storeEventsInLocalDB(response.events, siteId); @@ -1159,7 +1166,6 @@ export class AddonCalendarProvider { return site.getDb().getRecords(AddonCalendarProvider.EVENTS_TABLE, {repeatid: repeatId}); }); } - /** * Get monthly calendar events. * @@ -1167,10 +1173,12 @@ export class AddonCalendarProvider { * @param {number} month Month to get. * @param {number} [courseId] Course to get. * @param {number} [categoryId] Category to get. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the response. */ - getMonthlyEvents(year: number, month: number, courseId?: number, categoryId?: number, siteId?: string): Promise { + getMonthlyEvents(year: number, month: number, courseId?: number, categoryId?: number, ignoreCache?: boolean, siteId?: string) + : Promise { return this.sitesProvider.getSite(siteId).then((site) => { @@ -1180,7 +1188,7 @@ export class AddonCalendarProvider { }; // This parameter requires Moodle 3.5. - if ( site.isVersionGreaterEqualThan('3.5')) { + if (site.isVersionGreaterEqualThan('3.5')) { // Set mini to 1 to prevent returning the course selector HTML. data.mini = 1; } @@ -1192,11 +1200,16 @@ export class AddonCalendarProvider { data.categoryid = categoryId; } - const preSets = { + const preSets: CoreSiteWSPreSets = { cacheKey: this.getMonthlyEventsCacheKey(year, month, courseId, categoryId), updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('core_calendar_get_calendar_monthly_view', data, preSets).then((response) => { response.weeks.forEach((week) => { week.days.forEach((day) => { @@ -1253,10 +1266,11 @@ export class AddonCalendarProvider { * * @param {number} [courseId] Course to get. * @param {number} [categoryId] Category to get. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the response. */ - getUpcomingEvents(courseId?: number, categoryId?: number, siteId?: string): Promise { + getUpcomingEvents(courseId?: number, categoryId?: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { @@ -1269,11 +1283,16 @@ export class AddonCalendarProvider { data.categoryid = categoryId; } - const preSets = { + const preSets: CoreSiteWSPreSets = { cacheKey: this.getUpcomingEventsCacheKey(courseId, categoryId), updateFrequency: CoreSite.FREQUENCY_SOMETIMES }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('core_calendar_get_calendar_upcoming_view', data, preSets).then((response) => { this.storeEventsInLocalDB(response.events, siteId); diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index 36d1f6518..7fd222659 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -342,29 +342,36 @@ export class AddonCalendarHelperProvider { } /** - * Invalidate all calls from calendar WS calls. + * Refresh the month & day for several created/edited/deleted events, and invalidate the months & days + * for their repeated events if needed. * * @param {{event: any, repeated: number}[]} events Events that have been touched and number of times each event is repeated. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Resolved when done. */ - invalidateRepeatedEventsOnCalendar(events: {event: any, repeated: number}[], siteId?: string): Promise { + refreshAfterChangeEvents(events: {event: any, repeated: number}[], siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - const timestarts = []; + const fetchTimestarts = [], + invalidateTimestarts = []; + + // Always fetch upcoming events. + const upcomingPromise = this.calendarProvider.getUpcomingEvents(undefined, undefined, true, site.id).catch(() => { + // Ignore errors. + }); // Invalidate the events and get the timestarts so we can invalidate months & days. - return this.utils.allPromises(events.map((eventData) => { + return this.utils.allPromises([upcomingPromise].concat(events.map((eventData) => { if (eventData.repeated > 1) { if (eventData.event.repeatid) { // Being edited or deleted. // We need to calculate the days to invalidate because the event date could have changed. // We don't know if the repeated events are before or after this one, invalidate them all. - timestarts.push(eventData.event.timestart); + fetchTimestarts.push(eventData.event.timestart); for (let i = 1; i < eventData.repeated; i++) { - timestarts.push(eventData.event.timestart + CoreConstants.SECONDS_DAY * 7 * i); - timestarts.push(eventData.event.timestart - CoreConstants.SECONDS_DAY * 7 * i); + invalidateTimestarts.push(eventData.event.timestart + CoreConstants.SECONDS_DAY * 7 * i); + invalidateTimestarts.push(eventData.event.timestart - CoreConstants.SECONDS_DAY * 7 * i); } // Get the repeated events to invalidate them. @@ -378,48 +385,66 @@ export class AddonCalendarHelperProvider { } else { // Being added. let time = eventData.event.timestart; - while (eventData.repeated > 0) { - timestarts.push(time); + fetchTimestarts.push(time); + + while (eventData.repeated > 1) { time += CoreConstants.SECONDS_DAY * 7; eventData.repeated--; + invalidateTimestarts.push(time); } return Promise.resolve(); } } else { // Not repeated. - timestarts.push(eventData.event.timestart); + fetchTimestarts.push(eventData.event.timestart); return this.calendarProvider.invalidateEvent(eventData.event.id); } - })).finally(() => { - const invalidatedMonths = {}, - invalidatedDays = {}; + }))).finally(() => { + const treatedMonths = {}, + treatedDays = {}; return this.utils.allPromises([ this.calendarProvider.invalidateAllUpcomingEvents(), - // Invalidate months and days. - this.utils.allPromises(timestarts.map((time) => { + // Fetch or invalidate months and days. + this.utils.allPromises(fetchTimestarts.concat(invalidateTimestarts).map((time, index) => { const promises = [], day = moment(new Date(time * 1000)), monthId = this.getMonthId(day.year(), day.month() + 1), dayId = monthId + '#' + day.date(); - if (!invalidatedMonths[monthId]) { - // Month not invalidated already, do it now. - invalidatedMonths[monthId] = monthId; + if (!treatedMonths[monthId]) { + // Month not treated already, do it now. + treatedMonths[monthId] = monthId; - promises.push(this.calendarProvider.invalidateMonthlyEvents(day.year(), day.month() + 1, site.id)); + if (index < fetchTimestarts.length) { + promises.push(this.calendarProvider.getMonthlyEvents(day.year(), day.month() + 1, undefined, + undefined, true, site.id).catch(() => { + + // Ignore errors. + })); + } else { + promises.push(this.calendarProvider.invalidateMonthlyEvents(day.year(), day.month() + 1, site.id)); + } } - if (!invalidatedDays[dayId]) { + if (!treatedDays[dayId]) { // Day not invalidated already, do it now. - invalidatedDays[dayId] = dayId; + treatedDays[dayId] = dayId; - promises.push(this.calendarProvider.invalidateDayEvents(day.year(), day.month() + 1, day.date(), - site.id)); + if (index < fetchTimestarts.length) { + promises.push(this.calendarProvider.getDayEvents(day.year(), day.month() + 1, day.date(), + undefined, undefined, true, site.id).catch(() => { + + // Ignore errors. + })); + } else { + promises.push(this.calendarProvider.invalidateDayEvents(day.year(), day.month() + 1, day.date(), + site.id)); + } } return this.utils.allPromises(promises); @@ -430,14 +455,15 @@ export class AddonCalendarHelperProvider { } /** - * Invalidate all calls from calendar WS calls. + * Refresh the month & day for a created/edited/deleted event, and invalidate the months & days + * for their repeated events if needed. * * @param {any} event Event that has been touched. * @param {number} repeated Number of times the event is repeated. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Resolved when done. */ - invalidateRepeatedEventsOnCalendarForEvent(event: any, repeated: number, siteId?: string): Promise { - return this.invalidateRepeatedEventsOnCalendar([{event: event, repeated: repeated}], siteId); + refreshAfterChangeEvent(event: any, repeated: number, siteId?: string): Promise { + return this.refreshAfterChangeEvents([{event: event, repeated: repeated}], siteId); } }