diff --git a/lib/Controller/SessionController.php b/lib/Controller/SessionController.php
index d0cf5485a..c42cb7b46 100644
--- a/lib/Controller/SessionController.php
+++ b/lib/Controller/SessionController.php
@@ -80,6 +80,7 @@ class SessionController extends ApiController {
/**
* delete a session if existing
* @NoAdminRequired
+ * @NoCSRFRequired
* @param $boardId
* @return bool
*/
diff --git a/src/components/Controls.vue b/src/components/Controls.vue
index e1f55daba..a29cc5a2e 100644
--- a/src/components/Controls.vue
+++ b/src/components/Controls.vue
@@ -227,7 +227,7 @@ import FilterOffIcon from 'vue-material-design-icons/FilterOff.vue'
import ArrowCollapseVerticalIcon from 'vue-material-design-icons/ArrowCollapseVertical.vue'
import ArrowExpandVerticalIcon from 'vue-material-design-icons/ArrowExpandVertical.vue'
import SessionList from './SessionList'
-import { isNotifyPushEnabled } from '../listeners'
+import { isNotifyPushEnabled } from '../sessions'
export default {
name: 'Controls',
diff --git a/src/components/board/Board.vue b/src/components/board/Board.vue
index 1bc019ab0..05e8be3db 100644
--- a/src/components/board/Board.vue
+++ b/src/components/board/Board.vue
@@ -81,8 +81,7 @@ 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'
+import { createSession } from '../../sessions.js'
export default {
name: 'Board',
@@ -131,13 +130,11 @@ export default {
},
watch: {
id(newValue, oldValue) {
- if (oldValue) {
+ if (this.session) {
// close old session
- sessionApi.closeSession(oldValue, this.token)
- this.token = null
+ this.session.close()
}
- // create new session
- this.ensureSession(newValue)
+ this.session = createSession(newValue)
this.fetchData()
},
@@ -146,35 +143,11 @@ export default {
},
},
created() {
- if (isNotifyPushEnabled()) {
- // create a session
- this.ensureSession()
- }
-
+ this.session = createSession(this.id)
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)
- }
+ this.session.close()
},
methods: {
async fetchData() {
@@ -189,28 +162,6 @@ 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)
},
diff --git a/src/listeners.js b/src/listeners.js
deleted file mode 100644
index 4f99ebf10..000000000
--- a/src/listeners.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * @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 d7857426b..8f519b11e 100644
--- a/src/main.js
+++ b/src/main.js
@@ -31,7 +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'
+import './sessions.js'
// 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
index 33c52f4e6..2bcf9d4d0 100644
--- a/src/services/SessionApi.js
+++ b/src/services/SessionApi.js
@@ -40,6 +40,18 @@ export class SessionApi {
return await axios.post(this.url('/session/close'), { boardId, token })
}
+ async closeSessionViaBeacon(boardId, token) {
+ const body = {
+ boardId,
+ token,
+ }
+ const headers = {
+ type: 'application/json',
+ }
+ const blob = new Blob([JSON.stringify(body)], headers)
+ navigator.sendBeacon(this.url('/session/close'), blob)
+ }
+
}
export const sessionApi = new SessionApi()
diff --git a/src/sessions.js b/src/sessions.js
new file mode 100644
index 000000000..7b86fdb47
--- /dev/null
+++ b/src/sessions.js
@@ -0,0 +1,122 @@
+/*
+ * @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 { sessionApi } from './services/SessionApi'
+import store from './store/main'
+
+const SESSION_INTERVAL = 25 // in seconds
+
+let hasPush = false
+
+hasPush = listen('deck_board_update', (name, body) => {
+ triggerDeckReload(body.id)
+})
+
+/**
+ * is the notify_push app active and can
+ * provide us with real time updates?
+ */
+export function isNotifyPushEnabled() {
+ return hasPush
+}
+
+/**
+ * Triggers a reload of the deck, if the provided id
+ * matches the current open deck
+ *
+ * @param triggeredBoardId
+ */
+export function triggerDeckReload(triggeredBoardId) {
+ const currentBoardId = store.state.currentBoard?.id
+
+ // only handle update events for the currently open board
+ if (triggeredBoardId !== currentBoardId) return
+
+ store.dispatch('refreshBoard', currentBoardId)
+}
+
+/**
+ *
+ * @param boardId
+ */
+export function createSession(boardId) {
+
+ if (!isNotifyPushEnabled()) {
+ // return a dummy object
+ return {
+ async close() {},
+ }
+ }
+
+ // let's try to make createSession() synchronous, so that
+ // the component doesn't need to bother about the asynchronousness
+ let tokenPromise
+ let token
+ const create = () => {
+ tokenPromise = sessionApi.createSession(boardId).then(res => res.token)
+ tokenPromise.then((t) => {
+ token = t
+ })
+ }
+ create()
+
+ const ensureSession = async () => {
+ if (!tokenPromise) {
+ create()
+ return
+ }
+ try {
+ await sessionApi.syncSession(boardId, await tokenPromise)
+ } catch (err) {
+ // session probably expired, let's
+ // create a fresh session
+ create()
+ }
+ }
+
+ // periodically notify the server that we are still here
+ const interval = setInterval(ensureSession, SESSION_INTERVAL * 1000)
+
+ // close session when
+ const visibilitychangeListener = () => {
+ if (document.visibilityState === 'hidden') {
+ sessionApi.closeSessionViaBeacon(boardId, token)
+ tokenPromise = null
+ token = null
+ } else {
+ // tab is back in focus or was restored from the bfcache
+ ensureSession()
+
+ // we must assume that the websocket connection was
+ // paused and we have missed updates in the meantime.
+ triggerDeckReload()
+ }
+ }
+ document.addEventListener('visibilitychange', visibilitychangeListener)
+
+ return {
+ async close() {
+ clearInterval(interval)
+ document.removeEventListener('visibilitychange', visibilitychangeListener)
+ await sessionApi.closeSession(boardId, await tokenPromise)
+ },
+ }
+}