From f1ab3c4d1ae92ed9f8e881e1a56f80a627205f01 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 1 Aug 2022 16:31:03 +0800
Subject: [PATCH 1/7] Fix the JS error caused by some non-standard browsers

---
 web_src/js/features/eventsource.sharedworker.js | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/web_src/js/features/eventsource.sharedworker.js b/web_src/js/features/eventsource.sharedworker.js
index 824ccfea79f84..2ac7d93cc175e 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]) {

From 760bab538ad992ff271fc28d1138a3d705bacfb2 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 1 Aug 2022 18:44:52 +0800
Subject: [PATCH 2/7] fall back to periodic poller

---
 web_src/js/features/notification.js | 48 ++++++++--------
 web_src/js/features/stopwatch.js    | 87 +++++++++++++++--------------
 2 files changed, 70 insertions(+), 65 deletions(-)

diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js
index 36df196cac2d8..3a3a4060b1e8c 100644
--- a/web_src/js/features/notification.js
+++ b/web_src/js/features/notification.js
@@ -28,14 +28,11 @@ 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');
+    const notificationCounts = document.querySelectorAll('.notification_count');
+    for (const count of notificationCounts) {
+      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 +46,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 +71,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') {
+        console.error(`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 +98,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 +111,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 d63da4155af27..dcf8862b8273f 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,31 @@ 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 TimerPoller is used.
+  const currSeconds = $('.stopwatch-time').attr('data-seconds');
+  if (currSeconds) {
+    updateStopwatchTime(currSeconds);
+  }
+
+  let usingPeriodicPoller = false;
+  // poll the stopwatch status periodically
+  const startPeriodicPoller = (timeout) => {
+    if (timeout <= 0 || !Number.isFinite(timeout)) return;
+    usingPeriodicPoller = true;
+    setTimeout(async () => {
+      await 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 +57,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') {
+        console.error(`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 +84,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 +97,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 +118,6 @@ async function updateStopwatch() {
     url: `${appSubUrl}/user/stopwatches`,
     headers: {'X-Csrf-Token': csrfToken},
   });
-
-  if (updateTimeInterval) {
-    clearInterval(updateTimeInterval);
-    updateTimeInterval = null;
-  }
-
   return updateStopwatchData(data);
 }
 
@@ -127,10 +125,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 +134,32 @@ 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);
 }

From 5ff1e6479727cb292ffa2718dd45c21a1e4ab5ad Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 1 Aug 2022 18:48:07 +0800
Subject: [PATCH 3/7] inline the if, fix comments

---
 web_src/js/features/stopwatch.js | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/web_src/js/features/stopwatch.js b/web_src/js/features/stopwatch.js
index dcf8862b8273f..d463feb0c1c99 100644
--- a/web_src/js/features/stopwatch.js
+++ b/web_src/js/features/stopwatch.js
@@ -25,14 +25,13 @@ export function initStopwatch() {
     $(this).parent().trigger('submit');
   });
 
-  // global stop watch (in the head_navbar), it should always work in any case either the EventSource or the TimerPoller is used.
+  // 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;
-  // poll the stopwatch status periodically
   const startPeriodicPoller = (timeout) => {
     if (timeout <= 0 || !Number.isFinite(timeout)) return;
     usingPeriodicPoller = true;
@@ -149,9 +148,8 @@ function clearStopwatchTimer() {
 }
 function updateStopwatchTime(seconds) {
   const secs = parseInt(seconds);
-  if (!Number.isFinite(secs)) {
-    return;
-  }
+  if (!Number.isFinite(secs)) return;
+
   clearStopwatchTimer();
   const $stopwatch = $('.stopwatch-time');
   const start = Date.now();

From 6dbbd80d5dbf1614eb26d8d40603bc75b72aaefb Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 2 Aug 2022 09:08:01 +0800
Subject: [PATCH 4/7] Update web_src/js/features/stopwatch.js

---
 web_src/js/features/stopwatch.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/features/stopwatch.js b/web_src/js/features/stopwatch.js
index d463feb0c1c99..df43b7c507793 100644
--- a/web_src/js/features/stopwatch.js
+++ b/web_src/js/features/stopwatch.js
@@ -62,7 +62,7 @@ export function initStopwatch() {
       if (event.data.type === 'stopwatches') {
         updateStopwatchData(JSON.parse(event.data.data));
       } else if (event.data.type === 'no-event-source') {
-        console.error(`browser doesn't support EventSource, falling back to periodic poller`);
+        // 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);

From 23a293a7bf9377560aff90d33baadeb0ffa97d3f Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 2 Aug 2022 09:08:22 +0800
Subject: [PATCH 5/7] Update web_src/js/features/notification.js

---
 web_src/js/features/notification.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js
index 3a3a4060b1e8c..d7ca6df059edb 100644
--- a/web_src/js/features/notification.js
+++ b/web_src/js/features/notification.js
@@ -77,7 +77,7 @@ export function initNotificationCount() {
       if (event.data.type === 'notification-count') {
         const _promise = receiveUpdateCount(event.data);
       } else if (event.data.type === 'no-event-source') {
-        console.error(`browser doesn't support EventSource, falling back to periodic poller`);
+        // 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);

From a6aea838bcb8319e83ac454fa52b1b60f2886a9d Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 4 Aug 2022 00:19:19 +0800
Subject: [PATCH 6/7] Update web_src/js/features/notification.js

Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/features/notification.js | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js
index d7ca6df059edb..664a9c5c52553 100644
--- a/web_src/js/features/notification.js
+++ b/web_src/js/features/notification.js
@@ -28,8 +28,7 @@ async function receiveUpdateCount(event) {
   try {
     const data = JSON.parse(event.data);
 
-    const notificationCounts = document.querySelectorAll('.notification_count');
-    for (const count of notificationCounts) {
+    for (const count of document.querySelectorAll('.notification_count')) {
       count.classList.toggle('hidden', data.Count === 0);
       count.textContent = `${data.Count}`;
     }

From 644af7db3335e7f8cb36671db3290f4638b2a0d1 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 4 Aug 2022 00:19:52 +0800
Subject: [PATCH 7/7] Update web_src/js/features/stopwatch.js

Co-authored-by: silverwind <me@silverwind.io>
---
 web_src/js/features/stopwatch.js | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/web_src/js/features/stopwatch.js b/web_src/js/features/stopwatch.js
index df43b7c507793..c3aa79b7676e3 100644
--- a/web_src/js/features/stopwatch.js
+++ b/web_src/js/features/stopwatch.js
@@ -35,9 +35,7 @@ export function initStopwatch() {
   const startPeriodicPoller = (timeout) => {
     if (timeout <= 0 || !Number.isFinite(timeout)) return;
     usingPeriodicPoller = true;
-    setTimeout(async () => {
-      await updateStopwatchWithCallback(startPeriodicPoller, timeout);
-    }, timeout);
+    setTimeout(() => updateStopwatchWithCallback(startPeriodicPoller, timeout), timeout);
   };
 
   // if the browser supports EventSource and SharedWorker, use it instead of the periodic poller