code.gitea.io/gitea@v1.22.3/web_src/js/features/stopwatch.js (about)

     1  import {createTippy} from '../modules/tippy.js';
     2  import {GET} from '../modules/fetch.js';
     3  import {hideElem, showElem} from '../utils/dom.js';
     4  import {logoutFromWorker} from '../modules/worker.js';
     5  
     6  const {appSubUrl, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config;
     7  
     8  export function initStopwatch() {
     9    if (!enableTimeTracking) {
    10      return;
    11    }
    12  
    13    const stopwatchEls = document.querySelectorAll('.active-stopwatch');
    14    const stopwatchPopup = document.querySelector('.active-stopwatch-popup');
    15  
    16    if (!stopwatchEls.length || !stopwatchPopup) {
    17      return;
    18    }
    19  
    20    // global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used.
    21    const seconds = stopwatchEls[0]?.getAttribute('data-seconds');
    22    if (seconds) {
    23      updateStopwatchTime(parseInt(seconds));
    24    }
    25  
    26    for (const stopwatchEl of stopwatchEls) {
    27      stopwatchEl.removeAttribute('href'); // intended for noscript mode only
    28  
    29      createTippy(stopwatchEl, {
    30        content: stopwatchPopup.cloneNode(true),
    31        placement: 'bottom-end',
    32        trigger: 'click',
    33        maxWidth: 'none',
    34        interactive: true,
    35        hideOnClick: true,
    36        theme: 'default',
    37      });
    38    }
    39  
    40    let usingPeriodicPoller = false;
    41    const startPeriodicPoller = (timeout) => {
    42      if (timeout <= 0 || !Number.isFinite(timeout)) return;
    43      usingPeriodicPoller = true;
    44      setTimeout(() => updateStopwatchWithCallback(startPeriodicPoller, timeout), timeout);
    45    };
    46  
    47    // if the browser supports EventSource and SharedWorker, use it instead of the periodic poller
    48    if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) {
    49      // Try to connect to the event source via the shared worker first
    50      const worker = new SharedWorker(`${__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, 'notification-worker');
    51      worker.addEventListener('error', (event) => {
    52        console.error('worker error', event);
    53      });
    54      worker.port.addEventListener('messageerror', () => {
    55        console.error('unable to deserialize message');
    56      });
    57      worker.port.postMessage({
    58        type: 'start',
    59        url: `${window.location.origin}${appSubUrl}/user/events`,
    60      });
    61      worker.port.addEventListener('message', (event) => {
    62        if (!event.data || !event.data.type) {
    63          console.error('unknown worker message event', event);
    64          return;
    65        }
    66        if (event.data.type === 'stopwatches') {
    67          updateStopwatchData(JSON.parse(event.data.data));
    68        } else if (event.data.type === 'no-event-source') {
    69          // browser doesn't support EventSource, falling back to periodic poller
    70          if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout);
    71        } else if (event.data.type === 'error') {
    72          console.error('worker port event error', event.data);
    73        } else if (event.data.type === 'logout') {
    74          if (event.data.data !== 'here') {
    75            return;
    76          }
    77          worker.port.postMessage({
    78            type: 'close',
    79          });
    80          worker.port.close();
    81          logoutFromWorker();
    82        } else if (event.data.type === 'close') {
    83          worker.port.postMessage({
    84            type: 'close',
    85          });
    86          worker.port.close();
    87        }
    88      });
    89      worker.port.addEventListener('error', (e) => {
    90        console.error('worker port error', e);
    91      });
    92      worker.port.start();
    93      window.addEventListener('beforeunload', () => {
    94        worker.port.postMessage({
    95          type: 'close',
    96        });
    97        worker.port.close();
    98      });
    99  
   100      return;
   101    }
   102  
   103    startPeriodicPoller(notificationSettings.MinTimeout);
   104  }
   105  
   106  async function updateStopwatchWithCallback(callback, timeout) {
   107    const isSet = await updateStopwatch();
   108  
   109    if (!isSet) {
   110      timeout = notificationSettings.MinTimeout;
   111    } else if (timeout < notificationSettings.MaxTimeout) {
   112      timeout += notificationSettings.TimeoutStep;
   113    }
   114  
   115    callback(timeout);
   116  }
   117  
   118  async function updateStopwatch() {
   119    const response = await GET(`${appSubUrl}/user/stopwatches`);
   120    if (!response.ok) {
   121      console.error('Failed to fetch stopwatch data');
   122      return false;
   123    }
   124    const data = await response.json();
   125    return updateStopwatchData(data);
   126  }
   127  
   128  function updateStopwatchData(data) {
   129    const watch = data[0];
   130    const btnEls = document.querySelectorAll('.active-stopwatch');
   131    if (!watch) {
   132      hideElem(btnEls);
   133    } else {
   134      const {repo_owner_name, repo_name, issue_index, seconds} = watch;
   135      const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`;
   136      document.querySelector('.stopwatch-link')?.setAttribute('href', issueUrl);
   137      document.querySelector('.stopwatch-commit')?.setAttribute('action', `${issueUrl}/times/stopwatch/toggle`);
   138      document.querySelector('.stopwatch-cancel')?.setAttribute('action', `${issueUrl}/times/stopwatch/cancel`);
   139      const stopwatchIssue = document.querySelector('.stopwatch-issue');
   140      if (stopwatchIssue) stopwatchIssue.textContent = `${repo_owner_name}/${repo_name}#${issue_index}`;
   141      updateStopwatchTime(seconds);
   142      showElem(btnEls);
   143    }
   144    return Boolean(data.length);
   145  }
   146  
   147  // TODO: This flickers on page load, we could avoid this by making a custom
   148  // element to render time periods. Feeding a datetime in backend does not work
   149  // when time zone between server and client differs.
   150  function updateStopwatchTime(seconds) {
   151    if (!Number.isFinite(seconds)) return;
   152    const datetime = (new Date(Date.now() - seconds * 1000)).toISOString();
   153    for (const parent of document.querySelectorAll('.header-stopwatch-dot')) {
   154      const existing = parent.querySelector(':scope > relative-time');
   155      if (existing) {
   156        existing.setAttribute('datetime', datetime);
   157      } else {
   158        const el = document.createElement('relative-time');
   159        el.setAttribute('format', 'micro');
   160        el.setAttribute('datetime', datetime);
   161        el.setAttribute('lang', 'en-US');
   162        el.setAttribute('title', ''); // make <relative-time> show no title and therefor no tooltip
   163        parent.append(el);
   164      }
   165    }
   166  }