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 }