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

feat: sync calendar instantly on changes #6364

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
8 changes: 8 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,6 +54,10 @@
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
$context->registerEventListener(RenderReferenceEvent::class, CalendarReferenceListener::class);

$context->registerEventListener(CalendarObjectCreatedEvent::class, NotifyPushListener::class);

Check failure on line 57 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

UndefinedClass

lib/AppInfo/Application.php:57:35: UndefinedClass: Class, interface or enum named OCA\DAV\Events\CalendarObjectCreatedEvent does not exist (see https://psalm.dev/019)

Check failure on line 57 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable30

UndefinedClass

lib/AppInfo/Application.php:57:35: UndefinedClass: Class, interface or enum named OCA\DAV\Events\CalendarObjectCreatedEvent does not exist (see https://psalm.dev/019)
$context->registerEventListener(CalendarObjectUpdatedEvent::class, NotifyPushListener::class);

Check failure on line 58 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

UndefinedClass

lib/AppInfo/Application.php:58:35: UndefinedClass: Class, interface or enum named OCA\DAV\Events\CalendarObjectUpdatedEvent does not exist (see https://psalm.dev/019)

Check failure on line 58 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable30

UndefinedClass

lib/AppInfo/Application.php:58:35: UndefinedClass: Class, interface or enum named OCA\DAV\Events\CalendarObjectUpdatedEvent does not exist (see https://psalm.dev/019)
$context->registerEventListener(CalendarObjectDeletedEvent::class, NotifyPushListener::class);

Check failure on line 59 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

UndefinedClass

lib/AppInfo/Application.php:59:35: UndefinedClass: Class, interface or enum named OCA\DAV\Events\CalendarObjectDeletedEvent does not exist (see https://psalm.dev/019)

Check failure on line 59 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable30

UndefinedClass

lib/AppInfo/Application.php:59:35: UndefinedClass: Class, interface or enum named OCA\DAV\Events\CalendarObjectDeletedEvent does not exist (see https://psalm.dev/019)

$context->registerNotifierService(Notifier::class);
}

Expand Down
57 changes: 57 additions & 0 deletions lib/Listener/NotifyPushListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Calendar\Listener;

use OCA\DAV\Events\CalendarObjectCreatedEvent;
use OCA\DAV\Events\CalendarObjectDeletedEvent;
use OCA\DAV\Events\CalendarObjectUpdatedEvent;
use OCA\NotifyPush\Queue\IQueue;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IURLGenerator;
use OCP\IUserSession;

/**
* @template-implements IEventListener<Event|>
*/
class NotifyPushListener implements IEventListener {
public function __construct(
private readonly IUserSession $userSession,
private readonly IURLGenerator $urlGenerator,
private readonly ?IQueue $queue,

Check failure on line 28 in lib/Listener/NotifyPushListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

UndefinedClass

lib/Listener/NotifyPushListener.php:28:3: UndefinedClass: Class, interface or enum named OCA\NotifyPush\Queue\IQueue does not exist (see https://psalm.dev/019)

Check failure on line 28 in lib/Listener/NotifyPushListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable30

UndefinedClass

lib/Listener/NotifyPushListener.php:28:3: UndefinedClass: Class, interface or enum named OCA\NotifyPush\Queue\IQueue does not exist (see https://psalm.dev/019)
) {
}

/**
* @param CalendarObjectCreatedEvent|CalendarObjectUpdatedEvent|CalendarObjectDeletedEvent> $event
*/
public function handle(Event $event): void {

Check failure on line 35 in lib/Listener/NotifyPushListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

InvalidDocblock

lib/Listener/NotifyPushListener.php:35:2: InvalidDocblock: Invalid string CalendarObjectCreatedEvent|CalendarObjectUpdatedEvent|CalendarObjectDeletedEvent> $event in docblock for OCA\Calendar\Listener\NotifyPushListener::handle (see https://psalm.dev/008)

Check failure on line 35 in lib/Listener/NotifyPushListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable30

InvalidDocblock

lib/Listener/NotifyPushListener.php:35:2: InvalidDocblock: Invalid string CalendarObjectCreatedEvent|CalendarObjectUpdatedEvent|CalendarObjectDeletedEvent> $event in docblock for OCA\Calendar\Listener\NotifyPushListener::handle (see https://psalm.dev/008)
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'];

Check failure on line 48 in lib/Listener/NotifyPushListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

UndefinedMethod

lib/Listener/NotifyPushListener.php:48:18: UndefinedMethod: Method OCP\EventDispatcher\Event::getCalendarData does not exist (see https://psalm.dev/022)

Check failure on line 48 in lib/Listener/NotifyPushListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable30

UndefinedMethod

lib/Listener/NotifyPushListener.php:48:18: UndefinedMethod: Method OCP\EventDispatcher\Event::getCalendarData does not exist (see https://psalm.dev/022)
$this->queue->push('notify_custom', [

Check failure on line 49 in lib/Listener/NotifyPushListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

UndefinedClass

lib/Listener/NotifyPushListener.php:49:3: UndefinedClass: Class, interface or enum named OCA\NotifyPush\Queue\IQueue does not exist (see https://psalm.dev/019)

Check failure on line 49 in lib/Listener/NotifyPushListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable30

UndefinedClass

lib/Listener/NotifyPushListener.php:49:3: UndefinedClass: Class, interface or enum named OCA\NotifyPush\Queue\IQueue does not exist (see https://psalm.dev/019)
'user' => $user->getUID(),
'message' => 'calendar_sync',
'body' => [
'calendarUrl' => "$webroot/remote.php/dav/calendars/$uid/$uri/",
],
]);
}
}
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
42 changes: 42 additions & 0 deletions src/services/notifyService.js
Original file line number Diff line number Diff line change
@@ -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'

Check failure on line 7 in src/services/notifyService.js

View workflow job for this annotation

GitHub Actions / NPM lint

'loadState' is defined but never used
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() {

Check warning on line 14 in src/services/notifyService.js

View check run for this annotation

Codecov / codecov/patch

src/services/notifyService.js#L14

Added line #L14 was not covered by tests
// 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', {

Check warning on line 25 in src/services/notifyService.js

View check run for this annotation

Codecov / codecov/patch

src/services/notifyService.js#L23-L25

Added lines #L23 - L25 were not covered by tests
messageType,
messageBody,
})
const { calendarUrl } = messageBody
const calendar = calendarsStore.getCalendarByUrl(calendarUrl)

Check warning on line 30 in src/services/notifyService.js

View check run for this annotation

Codecov / codecov/patch

src/services/notifyService.js#L29-L30

Added lines #L29 - L30 were not covered by tests
if (!calendar) {
logger.warn(`Requested push sync for unknown calendar: ${calendarUrl}`, {

Check warning on line 32 in src/services/notifyService.js

View check run for this annotation

Codecov / codecov/patch

src/services/notifyService.js#L32

Added line #L32 was not covered by tests
messageType,
messageBody,
})
return

Check warning on line 36 in src/services/notifyService.js

View check run for this annotation

Codecov / codecov/patch

src/services/notifyService.js#L36

Added line #L36 was not covered by tests
}

logger.debug(`Syncing calendar ${calendarUrl} (requested by notify_push)`)
calendarsStore.syncCalendar({ calendar })

Check warning on line 40 in src/services/notifyService.js

View check run for this annotation

Codecov / codecov/patch

src/services/notifyService.js#L39-L40

Added lines #L39 - L40 were not covered by tests
})
}
38 changes: 38 additions & 0 deletions src/store/calendars.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
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'
Expand Down Expand Up @@ -940,5 +941,42 @@

this.syncTokens.set(calendar.id, syncToken)
},

syncCalendar({ calendar, skipIfUnchangedSyncToken = false }) {
const fetchedTimeRangesStore = useFetchedTimeRangesStore()
const calendarObjectsStore = useCalendarObjectsStore()
const calendarsStore = this

Check warning on line 948 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L945-L948

Added lines #L945 - L948 were not covered by tests

const existingSyncToken = calendarsStore.getCalendarSyncToken(calendar)

Check warning on line 950 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L950

Added line #L950 was not covered by tests
if (!existingSyncToken && !calendarsStore.getCalendarById(calendar.id)) {
// New calendar!
logger.debug(`Adding new calendar ${calendar.url}`)
calendarsStore.addCalendarMutation({ calendar })
return

Check warning on line 955 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L953-L955

Added lines #L953 - L955 were not covered by tests
}

if (skipIfUnchangedSyncToken && calendar.dav.syncToken === existingSyncToken) {
return

Check warning on line 959 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L959

Added line #L959 was not covered by tests
}

logger.debug(`Refetching calendar ${calendar.url} (syncToken changed)`)
const fetchedTimeRanges = fetchedTimeRangesStore

Check warning on line 963 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L962-L963

Added lines #L962 - L963 were not covered by tests
.getAllTimeRangesForCalendar(calendar.id)
for (const timeRange of fetchedTimeRanges) {
fetchedTimeRangesStore.removeTimeRange({

Check warning on line 966 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L965-L966

Added lines #L965 - L966 were not covered by tests
timeRangeId: timeRange.id,
})
calendarsStore.deleteFetchedTimeRangeFromCalendarMutation({

Check warning on line 969 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L969

Added line #L969 was not covered by tests
calendar,
fetchedTimeRangeId: timeRange.id,
})
}

calendarsStore.updateCalendarSyncToken({

Check warning on line 975 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L975

Added line #L975 was not covered by tests
calendar,
syncToken: calendar.dav.syncToken,
})
calendarObjectsStore.modificationCount++

Check warning on line 979 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L979

Added line #L979 was not covered by tests
},
},
})
34 changes: 4 additions & 30 deletions src/views/Calendar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
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',
Expand Down Expand Up @@ -210,41 +211,14 @@
},
},
created() {
registerNotifyPushSyncListener()

Check warning on line 214 in src/views/Calendar.vue

View check run for this annotation

Codecov / codecov/patch

src/views/Calendar.vue#L214

Added line #L214 was not covered by tests

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

View workflow job for this annotation

GitHub Actions / NPM lint

Trailing spaces not allowed
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)

Expand Down
Loading