code.gitea.io/gitea@v1.22.3/web_src/js/features/notification.js (about) 1 import $ from 'jquery'; 2 import {GET} from '../modules/fetch.js'; 3 import {toggleElem} from '../utils/dom.js'; 4 import {logoutFromWorker} from '../modules/worker.js'; 5 6 const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config; 7 let notificationSequenceNumber = 0; 8 9 export function initNotificationsTable() { 10 const table = document.getElementById('notification_table'); 11 if (!table) return; 12 13 // when page restores from bfcache, delete previously clicked items 14 window.addEventListener('pageshow', (e) => { 15 if (e.persisted) { // page was restored from bfcache 16 const table = document.getElementById('notification_table'); 17 const unreadCountEl = document.querySelector('.notifications-unread-count'); 18 let unreadCount = parseInt(unreadCountEl.textContent); 19 for (const item of table.querySelectorAll('.notifications-item[data-remove="true"]')) { 20 item.remove(); 21 unreadCount -= 1; 22 } 23 unreadCountEl.textContent = unreadCount; 24 } 25 }); 26 27 // mark clicked unread links for deletion on bfcache restore 28 for (const link of table.querySelectorAll('.notifications-item[data-status="1"] .notifications-link')) { 29 link.addEventListener('click', (e) => { 30 e.target.closest('.notifications-item').setAttribute('data-remove', 'true'); 31 }); 32 } 33 } 34 35 async function receiveUpdateCount(event) { 36 try { 37 const data = JSON.parse(event.data); 38 39 for (const count of document.querySelectorAll('.notification_count')) { 40 count.classList.toggle('tw-hidden', data.Count === 0); 41 count.textContent = `${data.Count}`; 42 } 43 await updateNotificationTable(); 44 } catch (error) { 45 console.error(error, event); 46 } 47 } 48 49 export function initNotificationCount() { 50 const $notificationCount = $('.notification_count'); 51 52 if (!$notificationCount.length) { 53 return; 54 } 55 56 let usingPeriodicPoller = false; 57 const startPeriodicPoller = (timeout, lastCount) => { 58 if (timeout <= 0 || !Number.isFinite(timeout)) return; 59 usingPeriodicPoller = true; 60 lastCount = lastCount ?? $notificationCount.text(); 61 setTimeout(async () => { 62 await updateNotificationCountWithCallback(startPeriodicPoller, timeout, lastCount); 63 }, timeout); 64 }; 65 66 if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) { 67 // Try to connect to the event source via the shared worker first 68 const worker = new SharedWorker(`${__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, 'notification-worker'); 69 worker.addEventListener('error', (event) => { 70 console.error('worker error', event); 71 }); 72 worker.port.addEventListener('messageerror', () => { 73 console.error('unable to deserialize message'); 74 }); 75 worker.port.postMessage({ 76 type: 'start', 77 url: `${window.location.origin}${appSubUrl}/user/events`, 78 }); 79 worker.port.addEventListener('message', (event) => { 80 if (!event.data || !event.data.type) { 81 console.error('unknown worker message event', event); 82 return; 83 } 84 if (event.data.type === 'notification-count') { 85 const _promise = receiveUpdateCount(event.data); 86 } else if (event.data.type === 'no-event-source') { 87 // browser doesn't support EventSource, falling back to periodic poller 88 if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout); 89 } else if (event.data.type === 'error') { 90 console.error('worker port event error', event.data); 91 } else if (event.data.type === 'logout') { 92 if (event.data.data !== 'here') { 93 return; 94 } 95 worker.port.postMessage({ 96 type: 'close', 97 }); 98 worker.port.close(); 99 logoutFromWorker(); 100 } else if (event.data.type === 'close') { 101 worker.port.postMessage({ 102 type: 'close', 103 }); 104 worker.port.close(); 105 } 106 }); 107 worker.port.addEventListener('error', (e) => { 108 console.error('worker port error', e); 109 }); 110 worker.port.start(); 111 window.addEventListener('beforeunload', () => { 112 worker.port.postMessage({ 113 type: 'close', 114 }); 115 worker.port.close(); 116 }); 117 118 return; 119 } 120 121 startPeriodicPoller(notificationSettings.MinTimeout); 122 } 123 124 async function updateNotificationCountWithCallback(callback, timeout, lastCount) { 125 const currentCount = $('.notification_count').text(); 126 if (lastCount !== currentCount) { 127 callback(notificationSettings.MinTimeout, currentCount); 128 return; 129 } 130 131 const newCount = await updateNotificationCount(); 132 let needsUpdate = false; 133 134 if (lastCount !== newCount) { 135 needsUpdate = true; 136 timeout = notificationSettings.MinTimeout; 137 } else if (timeout < notificationSettings.MaxTimeout) { 138 timeout += notificationSettings.TimeoutStep; 139 } 140 141 callback(timeout, newCount); 142 if (needsUpdate) { 143 await updateNotificationTable(); 144 } 145 } 146 147 async function updateNotificationTable() { 148 const notificationDiv = document.getElementById('notification_div'); 149 if (notificationDiv) { 150 try { 151 const params = new URLSearchParams(window.location.search); 152 params.set('div-only', true); 153 params.set('sequence-number', ++notificationSequenceNumber); 154 const url = `${appSubUrl}/notifications?${params.toString()}`; 155 const response = await GET(url); 156 157 if (!response.ok) { 158 throw new Error('Failed to fetch notification table'); 159 } 160 161 const data = await response.text(); 162 if ($(data).data('sequence-number') === notificationSequenceNumber) { 163 notificationDiv.outerHTML = data; 164 initNotificationsTable(); 165 } 166 } catch (error) { 167 console.error(error); 168 } 169 } 170 } 171 172 async function updateNotificationCount() { 173 try { 174 const response = await GET(`${appSubUrl}/notifications/new`); 175 176 if (!response.ok) { 177 throw new Error('Failed to fetch notification count'); 178 } 179 180 const data = await response.json(); 181 182 toggleElem('.notification_count', data.new !== 0); 183 184 for (const el of document.getElementsByClassName('notification_count')) { 185 el.textContent = `${data.new}`; 186 } 187 188 return `${data.new}`; 189 } catch (error) { 190 console.error(error); 191 return '0'; 192 } 193 }