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

     1  import $ from 'jquery';
     2  import prettyMilliseconds from 'pretty-ms';
     3  import {createTippy} from '../modules/tippy.js';
     4  
     5  const {appSubUrl, csrfToken, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config;
     6  
     7  export function initStopwatch() {
     8    if (!enableTimeTracking) {
     9      return;
    10    }
    11  
    12    const stopwatchEl = document.querySelector('.active-stopwatch-trigger');
    13    const stopwatchPopup = document.querySelector('.active-stopwatch-popup');
    14  
    15    if (!stopwatchEl || !stopwatchPopup) {
    16      return;
    17    }
    18  
    19    stopwatchEl.removeAttribute('href'); // intended for noscript mode only
    20  
    21    createTippy(stopwatchEl, {
    22      content: stopwatchPopup,
    23      placement: 'bottom-end',
    24      trigger: 'click',
    25      maxWidth: 'none',
    26      interactive: true,
    27      hideOnClick: true,
    28    });
    29  
    30    // global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used.
    31    const currSeconds = $('.stopwatch-time').attr('data-seconds');
    32    if (currSeconds) {
    33      updateStopwatchTime(currSeconds);
    34    }
    35  
    36    let usingPeriodicPoller = false;
    37    const startPeriodicPoller = (timeout) => {
    38      if (timeout <= 0 || !Number.isFinite(timeout)) return;
    39      usingPeriodicPoller = true;
    40      setTimeout(() => updateStopwatchWithCallback(startPeriodicPoller, timeout), timeout);
    41    };
    42  
    43    // if the browser supports EventSource and SharedWorker, use it instead of the periodic poller
    44    if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) {
    45      // Try to connect to the event source via the shared worker first
    46      const worker = new SharedWorker(`${__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, 'notification-worker');
    47      worker.addEventListener('error', (event) => {
    48        console.error('worker error', event);
    49      });
    50      worker.port.addEventListener('messageerror', () => {
    51        console.error('unable to deserialize message');
    52      });
    53      worker.port.postMessage({
    54        type: 'start',
    55        url: `${window.location.origin}${appSubUrl}/user/events`,
    56      });
    57      worker.port.addEventListener('message', (event) => {
    58        if (!event.data || !event.data.type) {
    59          console.error('unknown worker message event', event);
    60          return;
    61        }
    62        if (event.data.type === 'stopwatches') {
    63          updateStopwatchData(JSON.parse(event.data.data));
    64        } else if (event.data.type === 'no-event-source') {
    65          // browser doesn't support EventSource, falling back to periodic poller
    66          if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout);
    67        } else if (event.data.type === 'error') {
    68          console.error('worker port event error', event.data);
    69        } else if (event.data.type === 'logout') {
    70          if (event.data.data !== 'here') {
    71            return;
    72          }
    73          worker.port.postMessage({
    74            type: 'close',
    75          });
    76          worker.port.close();
    77          window.location.href = appSubUrl;
    78        } else if (event.data.type === 'close') {
    79          worker.port.postMessage({
    80            type: 'close',
    81          });
    82          worker.port.close();
    83        }
    84      });
    85      worker.port.addEventListener('error', (e) => {
    86        console.error('worker port error', e);
    87      });
    88      worker.port.start();
    89      window.addEventListener('beforeunload', () => {
    90        worker.port.postMessage({
    91          type: 'close',
    92        });
    93        worker.port.close();
    94      });
    95  
    96      return;
    97    }
    98  
    99    startPeriodicPoller(notificationSettings.MinTimeout);
   100  }
   101  
   102  async function updateStopwatchWithCallback(callback, timeout) {
   103    const isSet = await updateStopwatch();
   104  
   105    if (!isSet) {
   106      timeout = notificationSettings.MinTimeout;
   107    } else if (timeout < notificationSettings.MaxTimeout) {
   108      timeout += notificationSettings.TimeoutStep;
   109    }
   110  
   111    callback(timeout);
   112  }
   113  
   114  async function updateStopwatch() {
   115    const data = await $.ajax({
   116      type: 'GET',
   117      url: `${appSubUrl}/user/stopwatches`,
   118      headers: {'X-Csrf-Token': csrfToken},
   119    });
   120    return updateStopwatchData(data);
   121  }
   122  
   123  function updateStopwatchData(data) {
   124    const watch = data[0];
   125    const btnEl = $('.active-stopwatch-trigger');
   126    if (!watch) {
   127      clearStopwatchTimer();
   128      btnEl.addClass('gt-hidden');
   129    } else {
   130      const {repo_owner_name, repo_name, issue_index, seconds} = watch;
   131      const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`;
   132      $('.stopwatch-link').attr('href', issueUrl);
   133      $('.stopwatch-commit').attr('action', `${issueUrl}/times/stopwatch/toggle`);
   134      $('.stopwatch-cancel').attr('action', `${issueUrl}/times/stopwatch/cancel`);
   135      $('.stopwatch-issue').text(`${repo_owner_name}/${repo_name}#${issue_index}`);
   136      updateStopwatchTime(seconds);
   137      btnEl.removeClass('gt-hidden');
   138    }
   139    return Boolean(data.length);
   140  }
   141  
   142  let updateTimeIntervalId = null; // holds setInterval id when active
   143  function clearStopwatchTimer() {
   144    if (updateTimeIntervalId !== null) {
   145      clearInterval(updateTimeIntervalId);
   146      updateTimeIntervalId = null;
   147    }
   148  }
   149  function updateStopwatchTime(seconds) {
   150    const secs = parseInt(seconds);
   151    if (!Number.isFinite(secs)) return;
   152  
   153    clearStopwatchTimer();
   154    const $stopwatch = $('.stopwatch-time');
   155    const start = Date.now();
   156    const updateUi = () => {
   157      const delta = Date.now() - start;
   158      const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true});
   159      $stopwatch.text(dur);
   160    };
   161    updateUi();
   162    updateTimeIntervalId = setInterval(updateUi, 1000);
   163  }