diff --git a/lib/Service/ApiService.php b/lib/Service/ApiService.php index 48ee52d8efa..58f41cf1c34 100644 --- a/lib/Service/ApiService.php +++ b/lib/Service/ApiService.php @@ -29,6 +29,7 @@ use Exception; use InvalidArgumentException; use OCA\Files_Sharing\SharedStorage; +use OCA\NotifyPush\Queue\IQueue; use OCA\Text\AppInfo\Application; use OCA\Text\Db\Document; use OCA\Text\Db\Session; @@ -45,30 +46,22 @@ use OCP\IL10N; use OCP\IRequest; use OCP\Lock\LockedException; +use OCP\Server; use OCP\Share\IShare; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; use Psr\Log\LoggerInterface; class ApiService { - private IRequest $request; - private SessionService $sessionService; - private DocumentService $documentService; - private LoggerInterface $logger; - private EncodingService $encodingService; - private IL10N $l10n; - - public function __construct(IRequest $request, - SessionService $sessionService, - DocumentService $documentService, - EncodingService $encodingService, - LoggerInterface $logger, - IL10N $l10n + public function __construct( + private IRequest $request, + private SessionService $sessionService, + private DocumentService $documentService, + private EncodingService $encodingService, + private LoggerInterface $logger, + private IL10N $l10n, + private ?IQueue $queue, ) { - $this->request = $request; - $this->sessionService = $sessionService; - $this->documentService = $documentService; - $this->logger = $logger; - $this->encodingService = $encodingService; - $this->l10n = $l10n; } public function create(?int $fileId = null, ?string $filePath = null, ?string $baseVersionEtag = null, ?string $token = null, ?string $guestName = null): DataResponse { @@ -204,6 +197,7 @@ public function push(Session $session, Document $document, int $version, array $ } try { $result = $this->documentService->addStep($document, $session, $steps, $version, $token); + $this->addToPushQueue($document, [$awareness, ...array_values($steps)]); } catch (InvalidArgumentException $e) { return new DataResponse(['error' => $e->getMessage()], 422); } catch (DoesNotExistException|NotPermittedException) { @@ -213,6 +207,25 @@ public function push(Session $session, Document $document, int $version, array $ return new DataResponse($result); } + private function addToPushQueue(Document $document, array $steps): void { + if ($this->queue === null) { + return; + } + + $sessions = $this->sessionService->getActiveSessions($document->getId()); + $sessions = array_values(array_filter(array_unique(array_map(fn ($session): ?string => $session['userId'], $sessions)))); + foreach ($sessions as $userId) { + $this->queue->push('notify_custom', [ + 'user' => $userId, + 'message' => 'text_steps', + 'body' => [ + 'documentId' => $document->getId(), + 'steps' => $steps, + ], + ]); + } + } + public function sync(Session $session, Document $document, int $version = 0, ?string $shareToken = null): DataResponse { $documentId = $session->getDocumentId(); $result = []; diff --git a/package-lock.json b/package-lock.json index 6585d0539f9..814258764db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@nextcloud/l10n": "^2.2.0", "@nextcloud/logger": "^2.7.0", "@nextcloud/moment": "^1.3.1", + "@nextcloud/notify_push": "^1.1.4", "@nextcloud/router": "^3.0.0", "@nextcloud/vue": "^8.11.2", "@quartzy/markdown-it-mentions": "^0.2.0", @@ -4157,6 +4158,16 @@ "npm": "^9.0.0" } }, + "node_modules/@nextcloud/notify_push": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@nextcloud/notify_push/-/notify_push-1.1.4.tgz", + "integrity": "sha512-abWi1fD18XdJa5PstGK1E7/Q+XNbRjsF6jryp+Qa/qwP7x29CQzY33TVnA0IMWZeIA49Mjd3wZjmyf9BQmMJ2Q==", + "dependencies": { + "@nextcloud/axios": "^2.4.0", + "@nextcloud/capabilities": "^1.0.4", + "@nextcloud/event-bus": "^3.0.2" + } + }, "node_modules/@nextcloud/paths": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@nextcloud/paths/-/paths-2.1.0.tgz", @@ -33737,6 +33748,16 @@ "node-gettext": "^3.0.0" } }, + "@nextcloud/notify_push": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@nextcloud/notify_push/-/notify_push-1.1.4.tgz", + "integrity": "sha512-abWi1fD18XdJa5PstGK1E7/Q+XNbRjsF6jryp+Qa/qwP7x29CQzY33TVnA0IMWZeIA49Mjd3wZjmyf9BQmMJ2Q==", + "requires": { + "@nextcloud/axios": "^2.4.0", + "@nextcloud/capabilities": "^1.0.4", + "@nextcloud/event-bus": "^3.0.2" + } + }, "@nextcloud/paths": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@nextcloud/paths/-/paths-2.1.0.tgz", diff --git a/package.json b/package.json index 3d4f2de4c62..f04c8c00b2b 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@nextcloud/l10n": "^2.2.0", "@nextcloud/logger": "^2.7.0", "@nextcloud/moment": "^1.3.1", + "@nextcloud/notify_push": "^1.1.4", "@nextcloud/router": "^3.0.0", "@nextcloud/vue": "^8.11.2", "@quartzy/markdown-it-mentions": "^0.2.0", diff --git a/src/services/NotifyService.js b/src/services/NotifyService.js new file mode 100644 index 00000000000..e5608613215 --- /dev/null +++ b/src/services/NotifyService.js @@ -0,0 +1,34 @@ +/* + * @copyright Copyright (c) 2023 Julius Härtl + * + * @author Julius Härtl + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import mitt from 'mitt' +import { listen } from '@nextcloud/notify_push' + +if (!window._nc_text_notify) { + const useNotifyPush = listen('text_steps', (messageType, messageBody) => { + window._nc_text_notify?.emit('notify_push', { messageType, messageBody }) + }) + window._nc_text_notify = useNotifyPush ? mitt() : null +} + +export default () => { + return window._nc_text_notify +} diff --git a/src/services/PollingBackend.js b/src/services/PollingBackend.js index 3cbb3537f5e..60665b19920 100644 --- a/src/services/PollingBackend.js +++ b/src/services/PollingBackend.js @@ -22,6 +22,7 @@ import { logger } from '../helpers/logger.js' import { SyncService, ERROR_TYPE } from './SyncService.js' import { Connection } from './SessionApi.js' +import getNotifyBus from './NotifyService.js' /** * Minimum inverval to refetch the document changes @@ -50,7 +51,9 @@ const FETCH_INTERVAL_SINGLE_EDITOR = 5000 * * @type {number} time in ms */ -const FETCH_INTERVAL_INVISIBLE = 60000 +const FETCH_INTERVAL_INVISIBLE = 30000 + +const FETCH_INTERVAL_NOTIFY = 30000 /* Maximum number of retries for fetching before emitting a connection error */ const MAX_RETRY_FETCH_COUNT = 5 @@ -73,6 +76,7 @@ class PollingBackend { #fetchRetryCounter #pollActive #initialLoadingFinished + #notifyPushBus constructor(syncService, connection) { this.#syncService = syncService @@ -90,6 +94,7 @@ class PollingBackend { this.#initialLoadingFinished = false this.fetcher = setInterval(this._fetchSteps.bind(this), 50) document.addEventListener('visibilitychange', this.visibilitychange.bind(this)) + this.#notifyPushBus = getNotifyBus() } /** @@ -121,6 +126,13 @@ class PollingBackend { this.#pollActive = false } + handleNotifyPush({ messageType, messageBody }) { + if (messageBody.documentId !== this.#connection.document.id) { + return + } + this._handleResponse({ data: messageBody.response }) + } + _handleResponse({ data }) { const { document, sessions } = data this.#fetchRetryCounter = 0 @@ -198,15 +210,26 @@ class PollingBackend { } resetRefetchTimer() { + if (this.#notifyPushBus && this.#initialLoadingFinished) { + this.#fetchInterval = FETCH_INTERVAL_NOTIFY + return + } this.#fetchInterval = FETCH_INTERVAL - } increaseRefetchTimer() { + if (this.#notifyPushBus && this.#initialLoadingFinished) { + this.#fetchInterval = FETCH_INTERVAL_NOTIFY + return + } this.#fetchInterval = Math.min(this.#fetchInterval * 2, FETCH_INTERVAL_MAX) } maximumRefetchTimer() { + if (this.#notifyPushBus && this.#initialLoadingFinished) { + this.#fetchInterval = FETCH_INTERVAL_NOTIFY + return + } this.#fetchInterval = FETCH_INTERVAL_SINGLE_EDITOR } diff --git a/src/services/WebSocketPolyfill.js b/src/services/WebSocketPolyfill.js index 9273027bf2e..0c9ee4fcb26 100644 --- a/src/services/WebSocketPolyfill.js +++ b/src/services/WebSocketPolyfill.js @@ -22,6 +22,7 @@ import { logger } from '../helpers/logger.js' import { encodeArrayBuffer, decodeArrayBuffer } from '../helpers/base64.js' +import getNotifyBus from './NotifyService.js' /** * @@ -42,8 +43,11 @@ export default function initWebSocketPolyfill(syncService, fileId, initialSessio onclose onopen #handlers + #notifyPushBus constructor(url) { + this.#notifyPushBus = getNotifyBus() + this.#notifyPushBus?.on('notify_push', this.#onNotifyPush.bind(this)) this.url = url logger.debug('WebSocketPolyfill#constructor', { url, fileId, initialSession }) this.#registerHandlers({ @@ -118,6 +122,7 @@ export default function initWebSocketPolyfill(syncService, fileId, initialSessio Object.entries(this.#handlers) .forEach(([key, value]) => syncService.off(key, value)) this.#handlers = [] + this.#notifyPushBus?.off('notify_push', this.#onNotifyPush.bind(this)) syncService.close().then(() => { this.onclose() }) @@ -146,5 +151,15 @@ export default function initWebSocketPolyfill(syncService, fileId, initialSessio } } + #onNotifyPush({ messageType, messageBody }) { + if (messageBody.documentId !== fileId) { + return + } + messageBody.steps.forEach(step => { + const data = decodeArrayBuffer(step) + this.onmessage({ data }) + }) + } + } } diff --git a/tests/stub.php b/tests/stub.php index e1181c7ed9b..145971774e3 100644 --- a/tests/stub.php +++ b/tests/stub.php @@ -49,3 +49,15 @@ abstract public function setWantsNotification(bool $wantsNotification): void; abstract public function setNotificationTarget(?string $notificationTarget): void; } } + + +namespace OCA\NotifyPush\Queue { + interface IQueue { + /** + * @param string $channel + * @param mixed $message + * @return void + */ + public function push(string $channel, $message); + } +}