+
+
+
+
+
diff --git a/src/components/board/Board.vue b/src/components/board/Board.vue
index ae5492436..1bc019ab0 100644
--- a/src/components/board/Board.vue
+++ b/src/components/board/Board.vue
@@ -81,6 +81,8 @@ import Stack from './Stack.vue'
import { NcEmptyContent } from '@nextcloud/vue'
import GlobalSearchResults from '../search/GlobalSearchResults.vue'
import { showError } from '../../helpers/errors.js'
+import { sessionApi } from '../../services/SessionApi'
+import { isNotifyPushEnabled } from '../../listeners'
export default {
name: 'Board',
@@ -128,13 +130,51 @@ export default {
},
},
watch: {
- id: 'fetchData',
+ id(newValue, oldValue) {
+ if (oldValue) {
+ // close old session
+ sessionApi.closeSession(oldValue, this.token)
+ this.token = null
+ }
+ // create new session
+ this.ensureSession(newValue)
+
+ this.fetchData()
+ },
showArchived() {
this.fetchData()
},
},
created() {
+ if (isNotifyPushEnabled()) {
+ // create a session
+ this.ensureSession()
+ }
+
this.fetchData()
+
+ if (isNotifyPushEnabled()) {
+ // regularly let the server know that we are still here
+ this.sessionInterval = setInterval(() => {
+ this.ensureSession()
+ }, 25 * 1000)
+
+ // we don't get events pushed for sessions that have expired,
+ // so we poll the list of sessions every minute when there
+ // are other sessions active
+ this.refreshInterval = setInterval(() => {
+ if (this.board?.activeSessions?.length) {
+ this.refreshData()
+ }
+ }, 60 * 1000)
+ }
+ },
+ beforeDestroy() {
+ if (isNotifyPushEnabled()) {
+ sessionApi.closeSession(this.id, this.token)
+ clearInterval(this.sessionInterval)
+ clearInterval(this.refreshInterval)
+ }
},
methods: {
async fetchData() {
@@ -149,6 +189,32 @@ export default {
this.loading = false
},
+ async ensureSession(boardId = this.id) {
+ if (this.token) {
+ try {
+ await sessionApi.syncSession(boardId, this.token)
+ } catch (err) {
+ // session probably expired, let's try again
+ // with a fresh session
+ this.token = null
+ setTimeout(() => {
+ this.ensureSession()
+ }, 100)
+ }
+ } else {
+ try {
+ const res = await sessionApi.createSession(boardId)
+ this.token = res.token
+ } catch (err) {
+ showError(err)
+ }
+ }
+ },
+
+ async refreshData() {
+ await this.$store.dispatch('refreshBoard', this.id)
+ },
+
onDropStack({ removedIndex, addedIndex }) {
this.$store.dispatch('orderStack', { stack: this.stacksByBoard[removedIndex], removedIndex, addedIndex })
},
diff --git a/src/listeners.js b/src/listeners.js
new file mode 100644
index 000000000..4f99ebf10
--- /dev/null
+++ b/src/listeners.js
@@ -0,0 +1,41 @@
+/*
+ * @copyright Copyright (c) 2022, chandi Langecker (git@chandi.it)
+ *
+ * @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 { listen } from '@nextcloud/notify_push'
+import store from './store/main'
+
+let hasPush = false
+
+/**
+ * is the notify_push app active and can
+ * provide us with real time updates?
+ */
+export function isNotifyPushEnabled() {
+ return hasPush
+}
+
+hasPush = listen('DeckBoardUpdate', (name, body) => {
+ const currentBoardId = store.state.currentBoard?.id
+
+ // only handle update event for the currently open board
+ if (body.id !== currentBoardId) return
+
+ store.dispatch('refreshBoard', currentBoardId)
+})
diff --git a/src/main.js b/src/main.js
index 2d1a28132..d7857426b 100644
--- a/src/main.js
+++ b/src/main.js
@@ -31,6 +31,7 @@ import { subscribe } from '@nextcloud/event-bus'
import { Tooltip } from '@nextcloud/vue'
import ClickOutside from 'vue-click-outside'
import './models/index.js'
+import './listeners'
// the server snap.js conflicts with vertical scrolling so we disable it
document.body.setAttribute('data-snap-ignore', 'true')
diff --git a/src/services/SessionApi.js b/src/services/SessionApi.js
new file mode 100644
index 000000000..33c52f4e6
--- /dev/null
+++ b/src/services/SessionApi.js
@@ -0,0 +1,45 @@
+/*
+ * @copyright Copyright (c) 2022, chandi Langecker (git@chandi.it)
+ *
+ * @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 axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
+
+export class SessionApi {
+
+ url(url) {
+ url = `/apps/deck${url}`
+ return generateUrl(url)
+ }
+
+ async createSession(boardId) {
+ return (await axios.put(this.url('/session/create'), { boardId })).data
+ }
+
+ async syncSession(boardId, token) {
+ return await axios.post(this.url('/session/sync'), { boardId, token })
+ }
+
+ async closeSession(boardId, token) {
+ return await axios.post(this.url('/session/close'), { boardId, token })
+ }
+
+}
+
+export const sessionApi = new SessionApi()
diff --git a/tests/unit/Db/BoardTest.php b/tests/unit/Db/BoardTest.php
index 0c1814705..57f2492ad 100644
--- a/tests/unit/Db/BoardTest.php
+++ b/tests/unit/Db/BoardTest.php
@@ -32,6 +32,7 @@ class BoardTest extends TestCase {
'archived' => false,
'users' => ['user1', 'user2'],
'settings' => [],
+ 'activeSessions' => [],
'ETag' => $board->getETag(),
], $board->jsonSerialize());
}
@@ -55,6 +56,7 @@ class BoardTest extends TestCase {
'archived' => false,
'users' => ['user1', 'user2'],
'settings' => [],
+ 'activeSessions' => [],
'ETag' => $board->getETag(),
], $board->jsonSerialize());
}
@@ -76,6 +78,7 @@ class BoardTest extends TestCase {
'archived' => false,
'users' => [],
'settings' => [],
+ 'activeSessions' => [],
'ETag' => $board->getETag(),
], $board->jsonSerialize());
}
@@ -105,6 +108,7 @@ class BoardTest extends TestCase {
'shared' => 1,
'users' => [],
'settings' => [],
+ 'activeSessions' => [],
'ETag' => $board->getETag(),
], $board->jsonSerialize());
}
diff --git a/tests/unit/Service/BoardServiceTest.php b/tests/unit/Service/BoardServiceTest.php
index a54697132..69ac2fa5c 100644
--- a/tests/unit/Service/BoardServiceTest.php
+++ b/tests/unit/Service/BoardServiceTest.php
@@ -34,6 +34,8 @@ use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\ChangeHelper;
use OCA\Deck\Db\LabelMapper;
+use OCA\Deck\Db\Session;
+use OCA\Deck\Db\SessionMapper;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\Event\AclCreatedEvent;
use OCA\Deck\Event\AclDeletedEvent;
@@ -89,6 +91,8 @@ class BoardServiceTest extends TestCase {
private $connection;
/** @var BoardServiceValidator */
private $boardServiceValidator;
+ /** @var SessionMapper */
+ private $sessionMapper;
public function setUp(): void {
parent::setUp();
@@ -110,6 +114,7 @@ class BoardServiceTest extends TestCase {
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->connection = $this->createMock(IDBConnection::class);
$this->boardServiceValidator = $this->createMock(BoardServiceValidator::class);
+ $this->sessionMapper = $this->createMock(SessionMapper::class);
$this->service = new BoardService(
$this->boardMapper,
@@ -130,6 +135,7 @@ class BoardServiceTest extends TestCase {
$this->urlGenerator,
$this->connection,
$this->boardServiceValidator,
+ $this->sessionMapper,
$this->userId
);
@@ -172,6 +178,11 @@ class BoardServiceTest extends TestCase {
->willReturn([
'admin' => 'admin',
]);
+ $session = $this->createMock(Session::class);
+ $this->sessionMapper->expects($this->once())
+ ->method('findAllActive')
+ ->with(1)
+ ->willReturn([$session]);
$this->assertEquals($b1, $this->service->find(1));
}
@@ -224,6 +235,9 @@ class BoardServiceTest extends TestCase {
->willReturn([
'admin' => 'admin',
]);
+ $this->sessionMapper->expects($this->once())
+ ->method('findAllActive')
+ ->willReturn([]);
$b = $this->service->update(123, 'MyNewNameBoard', 'ffffff', false);
$this->assertEquals($b->getTitle(), 'MyNewNameBoard');
@@ -244,6 +258,10 @@ class BoardServiceTest extends TestCase {
->willReturn([
'admin' => 'admin',
]);
+ $this->sessionMapper->expects($this->once())
+ ->method('findAllActive')
+ ->with(null)
+ ->willReturn([]);
$boardDeleted = clone $board;
$boardDeleted->setDeletedAt(1);
$this->boardMapper->expects($this->once())