Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check for updates and update public calendars #2917

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/Controller/PublicViewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ private function publicIndex(string $token):PublicTemplateResponse {
$defaultTimezone = $this->config->getAppValue($this->appName, 'timezone', 'automatic');
$defaultSlotDuration = $this->config->getAppValue($this->appName, 'slotDuration', '00:30:00');
$defaultDefaultReminder = $this->config->getAppValue($this->appName, 'defaultReminder', 'none');
$defaultSyncTimeout = $this->config->getAppValue($this->appName, 'syncTimeout', 'PT1M');
$defaultShowTasks = $this->config->getAppValue($this->appName, 'showTasks', 'yes');
$defaultCanSubscribeLink = $this->config->getAppValue('dav', 'allow_calendar_link_subscriptions', 'yes');

Expand All @@ -136,6 +137,7 @@ private function publicIndex(string $token):PublicTemplateResponse {
$this->initialStateService->provideInitialState($this->appName, 'slot_duration', $defaultSlotDuration);
$this->initialStateService->provideInitialState($this->appName, 'default_reminder', $defaultDefaultReminder);
$this->initialStateService->provideInitialState($this->appName, 'show_tasks', $defaultShowTasks === 'yes');
$this->initialStateService->provideInitialState($this->appName, 'sync_timeout', $defaultSyncTimeout);
$this->initialStateService->provideInitialState($this->appName, 'tasks_enabled', false);
$this->initialStateService->provideInitialState($this->appName, 'hide_event_export', false);
$this->initialStateService->provideInitialState($this->appName, 'can_subscribe_link', $defaultCanSubscribeLink);
Expand Down
2 changes: 2 additions & 0 deletions lib/Controller/ViewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public function index():TemplateResponse {
$defaultReminder = $this->config->getUserValue($this->userId, $this->appName, 'defaultReminder', $defaultDefaultReminder);
$showTasks = $this->config->getUserValue($this->userId, $this->appName, 'showTasks', $defaultShowTasks) === 'yes';
$hideEventExport = $this->config->getAppValue($this->appName, 'hideEventExport', 'no') === 'yes';
$syncTimeout = $this->config->getAppValue($this->appName, 'syncTimeout', 'PT1M');
$disableAppointments = $this->config->getAppValue($this->appName, 'disableAppointments', 'no') === 'yes';
$forceEventAlarmType = $this->config->getAppValue($this->appName, 'forceEventAlarmType', '');
if (!in_array($forceEventAlarmType, ['DISPLAY', 'EMAIL'], true)) {
Expand All @@ -134,6 +135,7 @@ public function index():TemplateResponse {
$this->initialStateService->provideInitialState('talk_api_version', $talkApiVersion);
$this->initialStateService->provideInitialState('timezone', $timezone);
$this->initialStateService->provideInitialState('attachments_folder', $attachmentsFolder);
$this->initialStateService->provideInitialState('sync_timeout', $syncTimeout);
$this->initialStateService->provideInitialState('slot_duration', $slotDuration);
$this->initialStateService->provideInitialState('default_reminder', $defaultReminder);
$this->initialStateService->provideInitialState('show_tasks', $showTasks);
Expand Down
109 changes: 107 additions & 2 deletions src/store/calendars.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
*/
addCalendar(state, { calendar }) {
const object = getDefaultCalendarObject(calendar)

state.calendars.splice(object.dav.order, 0, object)
if (!state.calendars.some(existing => existing.id === object.id)) {
state.calendars.push(object)
}
Expand Down Expand Up @@ -578,6 +578,16 @@
},

editCalendarModal: (state) => state.editCalendarModal,
/**
* Gets all calendarObjects for a given calendar.
*
* @param {Object} state the store data
* @param {String} calendarId the id of the calendar
* @returns {function({String}): {Object}}
*/
getAllCalendarObjectsForCalendar: (state) => (calendarId) =>
state.calendarsById[calendarId].calendarObjects
,
}

const actions = {
Expand Down Expand Up @@ -611,8 +621,32 @@
}
},

async syncCalendars({ getters, dispatch }) {
const calendars = await dispatch('getNewAndUpdatedCalendars')
calendars.forEach(calendar => dispatch('updateCalendar', { calendar }))
},

/**
* Fetch calendars from the server and return the new ones and
* those that have changed compared to those currently in state.
*
* @param {Object} context the store mutations
* @returns {Promise<Array>} the calendars
*/
async getNewAndUpdatedCalendars({ commit, state, getters }) {
const calendars = await findAllCalendars()

Check failure on line 637 in src/store/calendars.js

View workflow job for this annotation

GitHub Actions / eslint

'findAllCalendars' is not defined
const principal = getters.getCurrentUserPrincipal
return calendars
.map((calendar) => mapDavCollectionToCalendar(calendar, principal))
.filter(retrieved => {
const stored = getters.getCalendarById(retrieved.id)
const updated = stored && (stored.dav.syncToken !== retrieved.dav.syncToken)
return !stored || updated
})
},

/**
* Retrieve and commit deleted calendars
* Retrieve and commit public calendars by token
*
* @param {object} context the store object
* @param {object} context.commit the store mutations
Expand Down Expand Up @@ -666,6 +700,30 @@
return calendarObjects
},

async syncPublicCalendars({ getters, dispatch }, { tokens }) {
const updated = await dispatch('getUpdatedPublicCalendars', { tokens })
updated.forEach(calendar => dispatch('updateCalendar', { calendar }))
},

/**
* Fetch all public calendars from the server and return those
* that have changed compared to those currently in state.
*
* @param {Object} vuex The destructuring object for vuex
* @param {Object} vuex.getters The Vuex getters Object
* @param {Object} data The data destructuring object
* @param {String[]} data.tokens The tokens to load
* @returns {Promise<Object[]>}
*/
async getUpdatedPublicCalendars({ getters }, { tokens }) {
const calendars = await findPublicCalendarsByTokens(tokens)
return calendars.map(mapDavCollectionToCalendar)
.filter(retrieved => {
const stored = getters.getCalendarById(retrieved.id)
return (stored.dav.syncToken !== retrieved.dav.syncToken)
})
},

/**
* Append a new calendar to array of existing calendars
*
Expand Down Expand Up @@ -868,6 +926,53 @@
context.commit('renameCalendar', { calendar, newName })
},

/**
* Update a calendar to a new state as fetched from the server
*
* This will primarily update the syncToken of the calendar
* and clear all cached data so that the calendar effectively
* reloads.
*
* @param {Object} vuex The destructuring object for vuex
* @param {Function} vuex.commit The Vuex commit function
* @param {Object} vuex.dispatch The Vuex dispatch
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to delete
*/
updateCalendar({ commit, dispatch }, { calendar }) {
dispatch('cleanupCalendar', calendar)
commit('addCalendar', { calendar })
},

/**
* Cleanup a calendar if it exists
*
* Remove the calendar from the current state including
* all cached data (TimeRanges and calendarObjects).
*
* @param {Object} vuex The destructuring object for vuex
* @param {Function} vuex.commit The Vuex commit function
* @param {Object} vuex.dispatch The Vuex dispatch
* @param {Object} calendar destructuring object
* @param {Object} calendar.id the id of the calendar to delete
*/
cleanupCalendar({ commit, getters }, { id }) {
const calendar = getters.getCalendarById(id)
if (calendar) {
commit('markCalendarAsLoading', { calendar })
getters.getAllTimeRangesForCalendar(calendar.id)
.forEach(timeRange => {
commit('removeTimeRange', { timeRangeId: timeRange.id })
})
getters.getAllCalendarObjectsForCalendar(calendar.id)
.forEach(id => {
const calendarObject = { id }
commit('deleteCalendarObject', { calendarObject })
})
commit('deleteCalendar', { calendar })
}
},

/**
* Change a calendar's color
*
Expand Down
4 changes: 2 additions & 2 deletions src/store/fetchedTimeRanges.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ const mutations = {
},

/**
* Adds a calendar-object-id to all time-ranges of a given caloendar
* Adds a calendar-object-id to all time-ranges of a given calendar
*
* @param {object} state The vuex state
* @param {object} data The destructuring object
Expand All @@ -171,7 +171,7 @@ const mutations = {
},

/**
* Removes a calendar-object-id to all time-ranges of a given caloendar
* Removes a calendar-object-id from all time-ranges of a given calendar
*
* @param {object} state The vuex state
* @param {object} data The destructuring object
Expand Down
15 changes: 14 additions & 1 deletion src/store/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { setConfig as setCalendarJsConfig } from '@nextcloud/calendar-js'
import { setConfig } from '../services/settings.js'
import { logInfo } from '../utils/logger.js'
import getTimezoneManager from '../services/timezoneDataProviderService.js'
import moment from '@nextcloud/moment'

const state = {
// env
Expand All @@ -42,6 +43,7 @@ const state = {
skipPopover: null,
slotDuration: null,
defaultReminder: null,
syncTimeout: null,
tasksEnabled: false,
timezone: 'automatic',
hideEventExport: false,
Expand Down Expand Up @@ -158,6 +160,7 @@ const mutations = {
* @param {boolean} data.skipPopover Whether or not to skip the simple event popover
* @param {string} data.slotDuration The duration of one slot in the agendaView
* @param {string} data.defaultReminder The default reminder to set on newly created events
* @param {String} data.syncTimeout The timeout between fetching updates from the server
* @param {boolean} data.talkEnabled Whether or not the talk app is enabled
* @param {boolean} data.tasksEnabled Whether ot not the tasks app is enabled
* @param {string} data.timezone The timezone to view the calendar in. Either an Olsen timezone or "automatic"
Expand All @@ -168,7 +171,7 @@ const mutations = {
* @param {string} data.attachmentsFolder Default user's attachments folder
* @param {boolean} data.showResources Show or hide the resources tab
*/
loadSettingsFromServer(state, { appVersion, eventLimit, firstRun, showWeekNumbers, showTasks, showWeekends, skipPopover, slotDuration, defaultReminder, talkEnabled, tasksEnabled, timezone, hideEventExport, forceEventAlarmType, disableAppointments, canSubscribeLink, attachmentsFolder, showResources }) {
loadSettingsFromServer(state, { appVersion, eventLimit, firstRun, showWeekNumbers, showTasks, showWeekends, skipPopover, slotDuration, syncTimeout, defaultReminder, talkEnabled, tasksEnabled, timezone, hideEventExport, forceEventAlarmType, disableAppointments, canSubscribeLink, attachmentsFolder, showResources }) {
logInfo(`
Initial settings:
- AppVersion: ${appVersion}
Expand All @@ -180,6 +183,7 @@ Initial settings:
- SkipPopover: ${skipPopover}
- SlotDuration: ${slotDuration}
- DefaultReminder: ${defaultReminder}
- SyncTimeout: ${syncTimeout}
- TalkEnabled: ${talkEnabled}
- TasksEnabled: ${tasksEnabled}
- Timezone: ${timezone}
Expand All @@ -199,6 +203,7 @@ Initial settings:
state.showWeekends = showWeekends
state.skipPopover = skipPopover
state.slotDuration = slotDuration
state.syncTimeout = syncTimeout
state.defaultReminder = defaultReminder
state.talkEnabled = talkEnabled
state.tasksEnabled = tasksEnabled
Expand Down Expand Up @@ -241,6 +246,14 @@ const getters = {
? detectTimezone()
: state.timezone,

/**
* Gets the sync timeout in milliseconds.
*
* @param {Object} state The Vuex state
* @returns {Integer}
*/
getSyncTimeout: (state) => moment.duration(state.syncTimeout).asMilliseconds(),

/**
* Gets the resolved timezone object.
* Falls back to UTC if timezone is invalid.
Expand Down
16 changes: 16 additions & 0 deletions src/views/Calendar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
loadingCalendars: true,
timeFrameCacheExpiryJob: null,
showEmptyCalendarScreen: false,
checkForUpdatesJob: null,
}
},
computed: {
Expand Down Expand Up @@ -205,6 +206,10 @@
}
}, 1000 * 60)
},
destroy() {
clearInterval(this.timeFrameCacheExpiryJob)
clearInterval(this.checkForUpdatesJob)
},
async beforeMount() {
this.$store.commit('loadSettingsFromServer', {
appVersion: loadState('calendar', 'app_version'),
Expand All @@ -219,6 +224,7 @@
tasksEnabled: loadState('calendar', 'tasks_enabled'),
timezone: loadState('calendar', 'timezone'),
showTasks: loadState('calendar', 'show_tasks'),
syncTimeout: loadState('calendar', 'sync_timeout'),
hideEventExport: loadState('calendar', 'hide_event_export'),
forceEventAlarmType: loadState('calendar', 'force_event_alarm_type', false),
disableAppointments: loadState('calendar', 'disable_appointments', false),
Expand Down Expand Up @@ -271,6 +277,16 @@

this.loadingCalendars = false
}
if (this.$store.getters.getSyncTimeout > 1000) {
this.checkForUpdatesJob = setInterval(async() => {

Check failure on line 281 in src/views/Calendar.vue

View workflow job for this annotation

GitHub Actions / eslint

Missing space before function parentheses
if (this.$route.name.startsWith('Public') || this.$route.name.startsWith('Embed')) {
const tokens = this.$route.params.tokens.split('-')
await this.$store.dispatch('syncPublicCalendars', { tokens })
} else {
await this.$store.dispatch('syncCalendars')
}
}, this.$store.getters.getSyncTimeout)
}
},
async mounted() {
if (this.timezone === 'automatic' && this.timezoneId === 'UTC') {
Expand Down
32 changes: 32 additions & 0 deletions tests/javascript/unit/store/settings.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ describe('store/settings test suite', () => {
showWeekNumbers: null,
skipPopover: null,
slotDuration: null,
syncTimeout: null,
defaultReminder: null,
tasksEnabled: false,
timezone: 'automatic',
Expand Down Expand Up @@ -168,6 +169,7 @@ describe('store/settings test suite', () => {
showWeekNumbers: null,
skipPopover: null,
slotDuration: null,
syncTimeout: null,
defaultReminder: null,
tasksEnabled: false,
timezone: 'automatic',
Expand All @@ -190,6 +192,7 @@ describe('store/settings test suite', () => {
showWeekends: true,
skipPopover: true,
slotDuration: '00:30:00',
syncTimeout: 'PT1M',
defaultReminder: '-600',
talkEnabled: false,
tasksEnabled: true,
Expand All @@ -216,6 +219,7 @@ Initial settings:
- ShowWeekends: true
- SkipPopover: true
- SlotDuration: 00:30:00
- SyncTimeout: PT1M
- DefaultReminder: -600
- TalkEnabled: false
- TasksEnabled: true
Expand All @@ -236,6 +240,7 @@ Initial settings:
showWeekends: true,
skipPopover: true,
slotDuration: '00:30:00',
syncTimeout: 'PT1M',
defaultReminder: '-600',
talkEnabled: false,
tasksEnabled: true,
Expand All @@ -262,6 +267,7 @@ Initial settings:
showWeekNumbers: null,
skipPopover: null,
slotDuration: null,
syncTimeout: null,
tasksEnabled: false,
timezone: 'automatic',
momentLocale: 'en',
Expand All @@ -282,6 +288,7 @@ Initial settings:
showWeekNumbers: null,
skipPopover: null,
slotDuration: null,
syncTimeout: null,
tasksEnabled: false,
timezone: 'automatic',
momentLocale: 'de',
Expand Down Expand Up @@ -312,6 +319,31 @@ Initial settings:
expect(detectTimezone).toHaveBeenCalledTimes(0)
})

it('should provide a getter for the sync timeout in milliseconds', () => {
const state = {
syncTimeout: '00:01:00'
}

expect(settingsStore.getters.getSyncTimeout(state)).toEqual(60 * 1000)
})

it('can parse sync timeouts in ISO 8601 format', () => {
const state = {
syncTimeout: 'PT1M'
}

expect(settingsStore.getters.getSyncTimeout(state)).toEqual(60 * 1000)
})

it('should handle invalid sync timeout gracefully', () => {
const state = {
syncTimeout: 'not a timeout at all'
}

// Check from Calendar.vue which will disable the updates:
expect(settingsStore.getters.getSyncTimeout(state) > 1000).toBeFalsy()
})

it('should provide an action to toggle the birthday calendar - enabled to disabled', async () => {
expect.assertions(3)

Expand Down
Loading
Loading