You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

notification.js 6.1KB

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