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  }