diff options
Diffstat (limited to 'web_src/js/features/notification.js')
-rw-r--r-- | web_src/js/features/notification.js | 192 |
1 files changed, 192 insertions, 0 deletions
diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js new file mode 100644 index 00000000..b57df9ce --- /dev/null +++ b/web_src/js/features/notification.js @@ -0,0 +1,192 @@ +import $ from 'jquery'; +import {GET} from '../modules/fetch.js'; +import {toggleElem} from '../utils/dom.js'; + +const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config; +let notificationSequenceNumber = 0; + +export function initNotificationsTable() { + const table = document.getElementById('notification_table'); + if (!table) return; + + // when page restores from bfcache, delete previously clicked items + window.addEventListener('pageshow', (e) => { + if (e.persisted) { // page was restored from bfcache + const table = document.getElementById('notification_table'); + const unreadCountEl = document.querySelector('.notifications-unread-count'); + let unreadCount = parseInt(unreadCountEl.textContent); + for (const item of table.querySelectorAll('.notifications-item[data-remove="true"]')) { + item.remove(); + unreadCount -= 1; + } + unreadCountEl.textContent = unreadCount; + } + }); + + // mark clicked unread links for deletion on bfcache restore + for (const link of table.querySelectorAll('.notifications-item[data-status="1"] .notifications-link')) { + link.addEventListener('click', (e) => { + e.target.closest('.notifications-item').setAttribute('data-remove', 'true'); + }); + } +} + +async function receiveUpdateCount(event) { + try { + const data = JSON.parse(event.data); + + for (const count of document.querySelectorAll('.notification_count')) { + count.classList.toggle('tw-hidden', data.Count === 0); + count.textContent = `${data.Count}`; + } + await updateNotificationTable(); + } catch (error) { + console.error(error, event); + } +} + +export function initNotificationCount() { + const $notificationCount = $('.notification_count'); + + if (!$notificationCount.length) { + return; + } + + let usingPeriodicPoller = false; + const startPeriodicPoller = (timeout, lastCount) => { + if (timeout <= 0 || !Number.isFinite(timeout)) return; + usingPeriodicPoller = true; + lastCount = lastCount ?? $notificationCount.text(); + setTimeout(async () => { + await updateNotificationCountWithCallback(startPeriodicPoller, timeout, lastCount); + }, timeout); + }; + + if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) { + // Try to connect to the event source via the shared worker first + const worker = new SharedWorker(`${__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, 'notification-worker'); + worker.addEventListener('error', (event) => { + console.error('worker error', event); + }); + worker.port.addEventListener('messageerror', () => { + console.error('unable to deserialize message'); + }); + worker.port.postMessage({ + type: 'start', + url: `${window.location.origin}${appSubUrl}/user/events`, + }); + worker.port.addEventListener('message', (event) => { + if (!event.data || !event.data.type) { + console.error('unknown worker message event', event); + return; + } + if (event.data.type === 'notification-count') { + const _promise = receiveUpdateCount(event.data); + } else if (event.data.type === 'no-event-source') { + // browser doesn't support EventSource, falling back to periodic poller + if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout); + } else if (event.data.type === 'error') { + console.error('worker port event error', event.data); + } else if (event.data.type === 'logout') { + if (event.data.data !== 'here') { + return; + } + worker.port.postMessage({ + type: 'close', + }); + worker.port.close(); + window.location.href = `${window.location.origin}${appSubUrl}/`; + } else if (event.data.type === 'close') { + worker.port.postMessage({ + type: 'close', + }); + worker.port.close(); + } + }); + worker.port.addEventListener('error', (e) => { + console.error('worker port error', e); + }); + worker.port.start(); + window.addEventListener('beforeunload', () => { + worker.port.postMessage({ + type: 'close', + }); + worker.port.close(); + }); + + return; + } + + startPeriodicPoller(notificationSettings.MinTimeout); +} + +async function updateNotificationCountWithCallback(callback, timeout, lastCount) { + const currentCount = $('.notification_count').text(); + if (lastCount !== currentCount) { + callback(notificationSettings.MinTimeout, currentCount); + return; + } + + const newCount = await updateNotificationCount(); + let needsUpdate = false; + + if (lastCount !== newCount) { + needsUpdate = true; + timeout = notificationSettings.MinTimeout; + } else if (timeout < notificationSettings.MaxTimeout) { + timeout += notificationSettings.TimeoutStep; + } + + callback(timeout, newCount); + if (needsUpdate) { + await updateNotificationTable(); + } +} + +async function updateNotificationTable() { + const notificationDiv = document.getElementById('notification_div'); + if (notificationDiv) { + try { + const params = new URLSearchParams(window.location.search); + params.set('div-only', true); + params.set('sequence-number', ++notificationSequenceNumber); + const url = `${appSubUrl}/notifications?${params.toString()}`; + const response = await GET(url); + + if (!response.ok) { + throw new Error('Failed to fetch notification table'); + } + + const data = await response.text(); + if ($(data).data('sequence-number') === notificationSequenceNumber) { + notificationDiv.outerHTML = data; + initNotificationsTable(); + } + } catch (error) { + console.error(error); + } + } +} + +async function updateNotificationCount() { + try { + const response = await GET(`${appSubUrl}/notifications/new`); + + if (!response.ok) { + throw new Error('Failed to fetch notification count'); + } + + const data = await response.json(); + + toggleElem('.notification_count', data.new !== 0); + + for (const el of document.getElementsByClassName('notification_count')) { + el.textContent = `${data.new}`; + } + + return `${data.new}`; + } catch (error) { + console.error(error); + return '0'; + } +} |