diff --git a/lib/Service/ApiService.php b/lib/Service/ApiService.php index 421e3aa3647..f9efd4e1c8d 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; @@ -43,7 +44,10 @@ 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 { @@ -201,6 +205,7 @@ public function push(Session $session, Document $document, $version, $steps, $aw } try { $result = $this->documentService->addStep($document, $session, $steps, $version); + $this->addToPushQueue($session, $document, $version); } catch (InvalidArgumentException $e) { return new DataResponse($e->getMessage(), 422); } catch (DoesNotExistException $e) { @@ -210,6 +215,27 @@ public function push(Session $session, Document $document, $version, $steps, $aw return new DataResponse($result); } + private function addToPushQueue(Session $session, Document $document, int $version): void { + try { + $queue = Server::get(IQueue::class); + $syncResponse = $this->sync($session, $document, $version); + $sessions = $this->sessionService->getActiveSessions($document->getId()); + $sessions = array_values(array_unique(array_map(fn($session) => $session['userId'], $sessions))); + foreach ($sessions as $userId) { + // Get sync response + $queue->push('notify_custom', [ + 'user' => $userId, + 'message' => 'text_steps', + 'body' => [ + 'documentId' => $document->getId(), + 'response' => $syncResponse->getData(), + ], + ]); + } + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + } + } + public function sync(Session $session, Document $document, $version = 0, $autosaveContent = null, $documentState = null, bool $force = false, bool $manualSave = false, $token = null): DataResponse { $documentId = $session->getDocumentId(); try { diff --git a/package-lock.json b/package-lock.json index 175c537e9a4..7af25b44947 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@nextcloud/l10n": "^2.2.0", "@nextcloud/logger": "^2.5.0", "@nextcloud/moment": "^1.2.1", + "@nextcloud/notify_push": "^1.1.3", "@nextcloud/router": "^2.1.2", "@nextcloud/vue": "^8.0.0-beta.2", "@quartzy/markdown-it-mentions": "^0.2.0", @@ -3948,6 +3949,102 @@ "node-gettext": "^3.0.0" } }, + "node_modules/@nextcloud/notify_push": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@nextcloud/notify_push/-/notify_push-1.1.3.tgz", + "integrity": "sha512-dHu3mz2tcqFl43DBxRhbLRY5J8gGi/mwg9PgHbEtK9qDOZ4EFUUXDteWe+B4TCghDGh+xKS6U7oDC5txtF+JaQ==", + "dependencies": { + "@nextcloud/axios": "^1.11.0", + "@nextcloud/capabilities": "^1.0.4", + "@nextcloud/event-bus": "^3.0.2" + } + }, + "node_modules/@nextcloud/notify_push/node_modules/@nextcloud/auth": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-1.3.0.tgz", + "integrity": "sha512-GfwRM9W7hat4psNdAt74UHEV+drEXQ53klCVp6JpON66ZLPeK5eJ1LQuiQDkpUxZpqNeaumXjiB98h5cug/uQw==", + "dependencies": { + "@nextcloud/event-bus": "^1.1.3", + "@nextcloud/typings": "^0.2.2", + "core-js": "^3.6.4" + } + }, + "node_modules/@nextcloud/notify_push/node_modules/@nextcloud/auth/node_modules/@nextcloud/event-bus": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@nextcloud/event-bus/-/event-bus-1.3.0.tgz", + "integrity": "sha512-+U5MnCvfnNWvf0lvdqJg8F+Nm8wN+s9ayuBjtiEQxTAcootv7lOnlMgfreqF3l2T0Wet2uZh4JbFVUWf8l3w7g==", + "dependencies": { + "@types/semver": "^7.3.5", + "core-js": "^3.11.2", + "semver": "^7.3.5" + } + }, + "node_modules/@nextcloud/notify_push/node_modules/@nextcloud/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@nextcloud/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-NyaiSC2GX2CPaH/MUGGMTTTza/TW9ZqWNGWq6LJ+pLER8nqZ9BQkwJ5kXUYGo+i3cka68PO+9WhcDv4fSABpuQ==", + "dependencies": { + "@nextcloud/auth": "^1.3.0", + "axios": "^0.27.1", + "core-js": "^3.6.4" + }, + "engines": { + "node": "^16.0.0", + "npm": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@nextcloud/notify_push/node_modules/@nextcloud/typings": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@nextcloud/typings/-/typings-0.2.4.tgz", + "integrity": "sha512-49M8XUDQH27VIQE+13KrqSOYcyOsDUk6Yfw17jbBVtXFoDJ3YBSYYq8YaKeAM3Lz2JVbEpqQW9suAT+EyYSb6g==", + "dependencies": { + "@types/jquery": "2.0.54" + } + }, + "node_modules/@nextcloud/notify_push/node_modules/@types/jquery": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-2.0.54.tgz", + "integrity": "sha512-D/PomKwNkDfSKD13DEVQT/pq2TUjN54c6uB341fEZanIzkjfGe7UaFuuaLZbpEiS5j7Wk2MUHAZqZIoECw29lg==" + }, + "node_modules/@nextcloud/notify_push/node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/@nextcloud/notify_push/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nextcloud/notify_push/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nextcloud/notify_push/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/@nextcloud/router": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-2.1.2.tgz", @@ -5143,9 +5240,7 @@ "node_modules/@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", - "dev": true, - "peer": true + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==" }, "node_modules/@types/serve-index": { "version": "1.9.1", @@ -25254,6 +25349,93 @@ } } }, + "@nextcloud/notify_push": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@nextcloud/notify_push/-/notify_push-1.1.3.tgz", + "integrity": "sha512-dHu3mz2tcqFl43DBxRhbLRY5J8gGi/mwg9PgHbEtK9qDOZ4EFUUXDteWe+B4TCghDGh+xKS6U7oDC5txtF+JaQ==", + "requires": { + "@nextcloud/axios": "^1.11.0", + "@nextcloud/capabilities": "^1.0.4", + "@nextcloud/event-bus": "^3.0.2" + }, + "dependencies": { + "@nextcloud/auth": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-1.3.0.tgz", + "integrity": "sha512-GfwRM9W7hat4psNdAt74UHEV+drEXQ53klCVp6JpON66ZLPeK5eJ1LQuiQDkpUxZpqNeaumXjiB98h5cug/uQw==", + "requires": { + "@nextcloud/event-bus": "^1.1.3", + "@nextcloud/typings": "^0.2.2", + "core-js": "^3.6.4" + }, + "dependencies": { + "@nextcloud/event-bus": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@nextcloud/event-bus/-/event-bus-1.3.0.tgz", + "integrity": "sha512-+U5MnCvfnNWvf0lvdqJg8F+Nm8wN+s9ayuBjtiEQxTAcootv7lOnlMgfreqF3l2T0Wet2uZh4JbFVUWf8l3w7g==", + "requires": { + "@types/semver": "^7.3.5", + "core-js": "^3.11.2", + "semver": "^7.3.5" + } + } + } + }, + "@nextcloud/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@nextcloud/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-NyaiSC2GX2CPaH/MUGGMTTTza/TW9ZqWNGWq6LJ+pLER8nqZ9BQkwJ5kXUYGo+i3cka68PO+9WhcDv4fSABpuQ==", + "requires": { + "@nextcloud/auth": "^1.3.0", + "axios": "^0.27.1", + "core-js": "^3.6.4" + } + }, + "@nextcloud/typings": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@nextcloud/typings/-/typings-0.2.4.tgz", + "integrity": "sha512-49M8XUDQH27VIQE+13KrqSOYcyOsDUk6Yfw17jbBVtXFoDJ3YBSYYq8YaKeAM3Lz2JVbEpqQW9suAT+EyYSb6g==", + "requires": { + "@types/jquery": "2.0.54" + } + }, + "@types/jquery": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-2.0.54.tgz", + "integrity": "sha512-D/PomKwNkDfSKD13DEVQT/pq2TUjN54c6uB341fEZanIzkjfGe7UaFuuaLZbpEiS5j7Wk2MUHAZqZIoECw29lg==" + }, + "axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "requires": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "@nextcloud/router": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-2.1.2.tgz", @@ -26114,9 +26296,7 @@ "@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", - "dev": true, - "peer": true + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==" }, "@types/serve-index": { "version": "1.9.1", diff --git a/package.json b/package.json index 80be8ac36d0..f1b7632a3a2 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@nextcloud/l10n": "^2.2.0", "@nextcloud/logger": "^2.5.0", "@nextcloud/moment": "^1.2.1", + "@nextcloud/notify_push": "^1.1.3", "@nextcloud/router": "^2.1.2", "@nextcloud/vue": "^8.0.0-beta.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 86c6e14b95e..2eda218b0d1 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 @@ -52,6 +53,8 @@ const FETCH_INTERVAL_SINGLE_EDITOR = 5000 */ const FETCH_INTERVAL_INVISIBLE = 60000 +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,8 @@ class PollingBackend { this.#initialLoadingFinished = false this.fetcher = setInterval(this._fetchSteps.bind(this), 50) document.addEventListener('visibilitychange', this.visibilitychange.bind(this)) + this.#notifyPushBus = getNotifyBus() + this.#notifyPushBus?.on('notify_push', this.handleNotifyPush.bind(this)) } /** @@ -123,6 +129,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 @@ -191,21 +204,33 @@ class PollingBackend { } disconnect() { + this.#notifyPushBus?.off('notify_push', this.handleNotifyPush) clearInterval(this.fetcher) this.fetcher = 0 document.removeEventListener('visibilitychange', this.visibilitychange.bind(this)) } 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 }