diff --git a/web_src/js/features/eventsource.sharedworker.js b/web_src/js/features/eventsource.sharedworker.js index 824ccfea79..2ac7d93cc1 100644 --- a/web_src/js/features/eventsource.sharedworker.js +++ b/web_src/js/features/eventsource.sharedworker.js @@ -70,6 +70,13 @@ class Source { self.addEventListener('connect', (e) => { for (const port of e.ports) { port.addEventListener('message', (event) => { + if (!self.EventSource) { + // some browsers (like PaleMoon, Firefox<53) don't support EventSource in SharedWorkerGlobalScope. + // this event handler needs EventSource when doing "new Source(url)", so just post a message back to the caller, + // in case the caller would like to use a fallback method to do its work. + port.postMessage({type: 'no-event-source'}); + return; + } if (event.data.type === 'start') { const url = event.data.url; if (sourcesByUrl[url]) { diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js index 36df196cac..664a9c5c52 100644 --- a/web_src/js/features/notification.js +++ b/web_src/js/features/notification.js @@ -28,14 +28,10 @@ async function receiveUpdateCount(event) { try { const data = JSON.parse(event.data); - const notificationCount = document.querySelector('.notification_count'); - if (data.Count > 0) { - notificationCount.classList.remove('hidden'); - } else { - notificationCount.classList.add('hidden'); + for (const count of document.querySelectorAll('.notification_count')) { + count.classList.toggle('hidden', data.Count === 0); + count.textContent = `${data.Count}`; } - - notificationCount.textContent = `${data.Count}`; await updateNotificationTable(); } catch (error) { console.error(error, event); @@ -49,14 +45,24 @@ export function initNotificationCount() { return; } - if (notificationSettings.EventSourceUpdateTime > 0 && !!window.EventSource && window.SharedWorker) { + 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`, 'notification-worker'); worker.addEventListener('error', (event) => { - console.error(event); + console.error('worker error', event); }); worker.port.addEventListener('messageerror', () => { - console.error('Unable to deserialize message'); + console.error('unable to deserialize message'); }); worker.port.postMessage({ type: 'start', @@ -64,13 +70,16 @@ export function initNotificationCount() { }); worker.port.addEventListener('message', (event) => { if (!event.data || !event.data.type) { - console.error(event); + 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(event.data); + console.error('worker port event error', event.data); } else if (event.data.type === 'logout') { if (event.data.data !== 'here') { return; @@ -88,7 +97,7 @@ export function initNotificationCount() { } }); worker.port.addEventListener('error', (e) => { - console.error(e); + console.error('worker port error', e); }); worker.port.start(); window.addEventListener('beforeunload', () => { @@ -101,17 +110,7 @@ export function initNotificationCount() { return; } - if (notificationSettings.MinTimeout <= 0) { - return; - } - - const fn = (timeout, lastCount) => { - setTimeout(() => { - const _promise = updateNotificationCountWithCallback(fn, timeout, lastCount); - }, timeout); - }; - - fn(notificationSettings.MinTimeout, notificationCount.text()); + startPeriodicPoller(notificationSettings.MinTimeout); } async function updateNotificationCountWithCallback(callback, timeout, lastCount) { diff --git a/web_src/js/features/stopwatch.js b/web_src/js/features/stopwatch.js index d63da4155a..c3aa79b767 100644 --- a/web_src/js/features/stopwatch.js +++ b/web_src/js/features/stopwatch.js @@ -2,7 +2,6 @@ import $ from 'jquery'; import prettyMilliseconds from 'pretty-ms'; const {appSubUrl, csrfToken, notificationSettings, enableTimeTracking} = window.config; -let updateTimeInterval = null; // holds setInterval id when active export function initStopwatch() { if (!enableTimeTracking) { @@ -26,14 +25,28 @@ export function initStopwatch() { $(this).parent().trigger('submit'); }); - if (notificationSettings.EventSourceUpdateTime > 0 && !!window.EventSource && window.SharedWorker) { + // global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used. + const currSeconds = $('.stopwatch-time').attr('data-seconds'); + if (currSeconds) { + updateStopwatchTime(currSeconds); + } + + let usingPeriodicPoller = false; + const startPeriodicPoller = (timeout) => { + if (timeout <= 0 || !Number.isFinite(timeout)) return; + usingPeriodicPoller = true; + setTimeout(() => updateStopwatchWithCallback(startPeriodicPoller, timeout), timeout); + }; + + // if the browser supports EventSource and SharedWorker, use it instead of the periodic poller + 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`, 'notification-worker'); worker.addEventListener('error', (event) => { - console.error(event); + console.error('worker error', event); }); worker.port.addEventListener('messageerror', () => { - console.error('Unable to deserialize message'); + console.error('unable to deserialize message'); }); worker.port.postMessage({ type: 'start', @@ -41,13 +54,16 @@ export function initStopwatch() { }); worker.port.addEventListener('message', (event) => { if (!event.data || !event.data.type) { - console.error(event); + console.error('unknown worker message event', event); return; } if (event.data.type === 'stopwatches') { updateStopwatchData(JSON.parse(event.data.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(event.data); + console.error('worker port event error', event.data); } else if (event.data.type === 'logout') { if (event.data.data !== 'here') { return; @@ -65,7 +81,7 @@ export function initStopwatch() { } }); worker.port.addEventListener('error', (e) => { - console.error(e); + console.error('worker port error', e); }); worker.port.start(); window.addEventListener('beforeunload', () => { @@ -78,22 +94,7 @@ export function initStopwatch() { return; } - if (notificationSettings.MinTimeout <= 0) { - return; - } - - const fn = (timeout) => { - setTimeout(() => { - const _promise = updateStopwatchWithCallback(fn, timeout); - }, timeout); - }; - - fn(notificationSettings.MinTimeout); - - const currSeconds = $('.stopwatch-time').data('seconds'); - if (currSeconds) { - updateTimeInterval = updateStopwatchTime(currSeconds); - } + startPeriodicPoller(notificationSettings.MinTimeout); } async function updateStopwatchWithCallback(callback, timeout) { @@ -114,12 +115,6 @@ async function updateStopwatch() { url: `${appSubUrl}/user/stopwatches`, headers: {'X-Csrf-Token': csrfToken}, }); - - if (updateTimeInterval) { - clearInterval(updateTimeInterval); - updateTimeInterval = null; - } - return updateStopwatchData(data); } @@ -127,10 +122,7 @@ function updateStopwatchData(data) { const watch = data[0]; const btnEl = $('.active-stopwatch-trigger'); if (!watch) { - if (updateTimeInterval) { - clearInterval(updateTimeInterval); - updateTimeInterval = null; - } + clearStopwatchTimer(); btnEl.addClass('hidden'); } else { const {repo_owner_name, repo_name, issue_index, seconds} = watch; @@ -139,22 +131,31 @@ function updateStopwatchData(data) { $('.stopwatch-commit').attr('action', `${issueUrl}/times/stopwatch/toggle`); $('.stopwatch-cancel').attr('action', `${issueUrl}/times/stopwatch/cancel`); $('.stopwatch-issue').text(`${repo_owner_name}/${repo_name}#${issue_index}`); - $('.stopwatch-time').text(prettyMilliseconds(seconds * 1000)); - updateTimeInterval = updateStopwatchTime(seconds); + updateStopwatchTime(seconds); btnEl.removeClass('hidden'); } - return !!data.length; } +let updateTimeIntervalId = null; // holds setInterval id when active +function clearStopwatchTimer() { + if (updateTimeIntervalId !== null) { + clearInterval(updateTimeIntervalId); + updateTimeIntervalId = null; + } +} function updateStopwatchTime(seconds) { const secs = parseInt(seconds); - if (!Number.isFinite(secs)) return null; + if (!Number.isFinite(secs)) return; + clearStopwatchTimer(); + const $stopwatch = $('.stopwatch-time'); const start = Date.now(); - return setInterval(() => { + const updateUi = () => { const delta = Date.now() - start; const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true}); - $('.stopwatch-time').text(dur); - }, 1000); + $stopwatch.text(dur); + }; + updateUi(); + updateTimeIntervalId = setInterval(updateUi, 1000); }