diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 083efefad1..20769eb1b4 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -11,10 +11,14 @@ use OCA\Calendar\Events\BeforeAppointmentBookedEvent; use OCA\Calendar\Listener\AppointmentBookedListener; use OCA\Calendar\Listener\CalendarReferenceListener; +use OCA\Calendar\Listener\NotifyPushListener; use OCA\Calendar\Listener\UserDeletedListener; use OCA\Calendar\Notification\Notifier; use OCA\Calendar\Profile\AppointmentsAction; use OCA\Calendar\Reference\ReferenceProvider; +use OCA\DAV\Events\CalendarObjectCreatedEvent; +use OCA\DAV\Events\CalendarObjectDeletedEvent; +use OCA\DAV\Events\CalendarObjectUpdatedEvent; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; @@ -50,6 +54,10 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); $context->registerEventListener(RenderReferenceEvent::class, CalendarReferenceListener::class); + $context->registerEventListener(CalendarObjectCreatedEvent::class, NotifyPushListener::class); + $context->registerEventListener(CalendarObjectUpdatedEvent::class, NotifyPushListener::class); + $context->registerEventListener(CalendarObjectDeletedEvent::class, NotifyPushListener::class); + $context->registerNotifierService(Notifier::class); } diff --git a/lib/Listener/NotifyPushListener.php b/lib/Listener/NotifyPushListener.php new file mode 100644 index 0000000000..f161753597 --- /dev/null +++ b/lib/Listener/NotifyPushListener.php @@ -0,0 +1,57 @@ + + */ +class NotifyPushListener implements IEventListener { + public function __construct( + private readonly IUserSession $userSession, + private readonly IURLGenerator $urlGenerator, + private readonly ?IQueue $queue, + ) { + } + + /** + * @param CalendarObjectCreatedEvent|CalendarObjectUpdatedEvent|CalendarObjectDeletedEvent> $event + */ + public function handle(Event $event): void { + if ($this->queue === null) { + return; + } + + $user = $this->userSession->getUser(); + if ($user === null) { + return; + } + + // TODO: How to generate this in a more safe way? + $webroot = $this->urlGenerator->getWebroot(); + $uid = $user->getUID(); + $uri = $event->getCalendarData()['uri']; + $this->queue->push('notify_custom', [ + 'user' => $user->getUID(), + 'message' => 'calendar_sync', + 'body' => [ + 'calendarUrl' => "$webroot/remote.php/dav/calendars/$uid/$uri/", + ], + ]); + } +} diff --git a/package-lock.json b/package-lock.json index e95323b98c..bb8a24cac8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@nextcloud/l10n": "^3.1.0", "@nextcloud/logger": "^3.0.2", "@nextcloud/moment": "^1.3.1", + "@nextcloud/notify_push": "^1.3.0", "@nextcloud/router": "^3.0.1", "@nextcloud/vue": "^8.18.0", "autosize": "^6.0.1", @@ -3593,6 +3594,17 @@ "npm": "^10.0.0" } }, + "node_modules/@nextcloud/notify_push": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@nextcloud/notify_push/-/notify_push-1.3.0.tgz", + "integrity": "sha512-WmyINTP/RynrfrOdyxzcntwV79b88uhXHU3cVJEcMzuh7wt6YT66kitjuQHMGlrG/xlEwk4qUKEM/NpFqVcvJg==", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@nextcloud/axios": "^2.5.0", + "@nextcloud/capabilities": "^1.2.0", + "@nextcloud/event-bus": "^3.3.0" + } + }, "node_modules/@nextcloud/paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@nextcloud/paths/-/paths-2.2.1.tgz", diff --git a/package.json b/package.json index 861ad63bc4..1d34e05f9c 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@nextcloud/l10n": "^3.1.0", "@nextcloud/logger": "^3.0.2", "@nextcloud/moment": "^1.3.1", + "@nextcloud/notify_push": "^1.3.0", "@nextcloud/router": "^3.0.1", "@nextcloud/vue": "^8.18.0", "autosize": "^6.0.1", diff --git a/src/services/notifyService.js b/src/services/notifyService.js new file mode 100644 index 0000000000..16f384a787 --- /dev/null +++ b/src/services/notifyService.js @@ -0,0 +1,42 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { listen } from '@nextcloud/notify_push' +import { loadState } from '@nextcloud/initial-state' +import useCalendarsStore from '../store/calendars.js' +import logger from '../utils/logger.js' + +/** + * Register a notify_push listener to listen for sync requests and sync calendars. + */ +export function registerNotifyPushSyncListener() { + // TODO: how to actually get the state + /* + const isPushEnabled = loadState('calendar', 'notify_push_available', false) + if (!isPushEnabled) { + return + } + */ + + const calendarsStore = useCalendarsStore() + listen('calendar_sync', (messageType, messageBody) => { + logger.debug('calendar_sync', { + messageType, + messageBody, + }) + const { calendarUrl } = messageBody + const calendar = calendarsStore.getCalendarByUrl(calendarUrl) + if (!calendar) { + logger.warn(`Requested push sync for unknown calendar: ${calendarUrl}`, { + messageType, + messageBody, + }) + return + } + + logger.debug(`Syncing calendar ${calendarUrl} (requested by notify_push)`) + calendarsStore.syncCalendar({ calendar }) + }) +} diff --git a/src/store/calendars.js b/src/store/calendars.js index b8b8600815..5fd6db2708 100644 --- a/src/store/calendars.js +++ b/src/store/calendars.js @@ -29,6 +29,7 @@ import useSettingsStore from './settings.js' import useFetchedTimeRangesStore from './fetchedTimeRanges.js' import usePrincipalsStore from './principals.js' import useCalendarObjectsStore from './calendarObjects.js' +import logger from '../utils/logger.js' import { defineStore } from 'pinia' import Vue from 'vue' @@ -940,5 +941,42 @@ export default defineStore('calendars', { this.syncTokens.set(calendar.id, syncToken) }, + + syncCalendar({ calendar, skipIfUnchangedSyncToken = false }) { + const fetchedTimeRangesStore = useFetchedTimeRangesStore() + const calendarObjectsStore = useCalendarObjectsStore() + const calendarsStore = this + + const existingSyncToken = calendarsStore.getCalendarSyncToken(calendar) + if (!existingSyncToken && !calendarsStore.getCalendarById(calendar.id)) { + // New calendar! + logger.debug(`Adding new calendar ${calendar.url}`) + calendarsStore.addCalendarMutation({ calendar }) + return + } + + if (skipIfUnchangedSyncToken && calendar.dav.syncToken === existingSyncToken) { + return + } + + logger.debug(`Refetching calendar ${calendar.url} (syncToken changed)`) + const fetchedTimeRanges = fetchedTimeRangesStore + .getAllTimeRangesForCalendar(calendar.id) + for (const timeRange of fetchedTimeRanges) { + fetchedTimeRangesStore.removeTimeRange({ + timeRangeId: timeRange.id, + }) + calendarsStore.deleteFetchedTimeRangeFromCalendarMutation({ + calendar, + fetchedTimeRangeId: timeRange.id, + }) + } + + calendarsStore.updateCalendarSyncToken({ + calendar, + syncToken: calendar.dav.syncToken, + }) + calendarObjectsStore.modificationCount++ + }, }, }) diff --git a/src/views/Calendar.vue b/src/views/Calendar.vue index c84a7cde57..20b1795c55 100644 --- a/src/views/Calendar.vue +++ b/src/views/Calendar.vue @@ -99,6 +99,7 @@ import useSettingsStore from '../store/settings.js' import useWidgetStore from '../store/widget.js' import { mapStores, mapState } from 'pinia' import { mapDavCollectionToCalendar } from '../models/calendar.js' +import { registerNotifyPushSyncListener } from '../services/notifyService.js' export default { name: 'Calendar', @@ -210,41 +211,14 @@ export default { }, }, created() { + registerNotifyPushSyncListener() + this.backgroundSyncJob = setInterval(async () => { const currentUserPrincipal = this.principalsStore.getCurrentUserPrincipal const calendars = (await findAllCalendars()) .map((calendar) => mapDavCollectionToCalendar(calendar, currentUserPrincipal)) for (const calendar of calendars) { - const existingSyncToken = this.calendarsStore.getCalendarSyncToken(calendar) - if (!existingSyncToken && !this.calendarsStore.getCalendarById(calendar.id)) { - // New calendar! - logger.debug(`Adding new calendar ${calendar.url}`) - this.calendarsStore.addCalendarMutation({ calendar }) - continue - } - - if (calendar.dav.syncToken === existingSyncToken) { - continue - } - - logger.debug(`Refetching calendar ${calendar.url} (syncToken changed)`) - const fetchedTimeRanges = this.fetchedTimeRangesStore - .getAllTimeRangesForCalendar(calendar.id) - for (const timeRange of fetchedTimeRanges) { - this.fetchedTimeRangesStore.removeTimeRange({ - timeRangeId: timeRange.id, - }) - this.calendarsStore.deleteFetchedTimeRangeFromCalendarMutation({ - calendar, - fetchedTimeRangeId: timeRange.id, - }) - } - - this.calendarsStore.updateCalendarSyncToken({ - calendar, - syncToken: calendar.dav.syncToken, - }) - this.calendarObjectsStore.modificationCount++ + this.calendarsStore.syncCalendar({ calendar, skipIfUnchangedSyncToken: true }) } }, 1000 * 30)