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