Refined GitHub Notifications

Make GitHub Notifications Better

当前为 2023-03-06 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Refined GitHub Notifications
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1
  5. // @description Make GitHub Notifications Better
  6. // @author Anthony Fu
  7. // @match https://github.com/**
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=github.com
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. /* eslint-disable no-console */
  13.  
  14. (function () {
  15. 'use strict'
  16.  
  17. // Fix the archive link
  18. if (location.pathname === '/notifications/beta/archive')
  19. location.pathname = '/notifications'
  20.  
  21. const TIMEOUT = 60_000
  22. const NAME = 'Refined GitHub Notifications'
  23. let lastUpdate = Date.now()
  24.  
  25. let bc
  26. let bcInitTime = 0
  27.  
  28. function initBroadcastChannel() {
  29. bcInitTime = Date.now()
  30. bc = new BroadcastChannel('refined-github-notifications')
  31.  
  32. bc.onmessage = ({ data }) => {
  33. console.log(`[${NAME}]`, 'Received message', data)
  34. if (data.type === 'check-dedupe') {
  35. // If the new tab is opened after the current tab, close the current tab
  36. if (data.time > bcInitTime) {
  37. // TODO: close the tab
  38. try {
  39. window.close()
  40. }
  41. catch (e) {}
  42. location.href = 'about:blank'
  43. }
  44. }
  45. }
  46. }
  47.  
  48. function dedupeTab() {
  49. if (!bc)
  50. return
  51. bc.postMessage({ type: 'check-dedupe', time: bcInitTime, url: location.href })
  52. }
  53.  
  54. function externalize() {
  55. document.querySelectorAll('a')
  56. .forEach((r) => {
  57. if (r.href.startsWith('https://github.com/notifications'))
  58. return
  59. r.target = '_blank'
  60. r.rel = 'noopener noreferrer'
  61. const url = new URL(r.href)
  62.  
  63. // Remove notification_referrer_id
  64. if (url.searchParams.get('notification_referrer_id')) {
  65. url.searchParams.delete('notification_referrer_id')
  66. r.href = url.toString()
  67. }
  68. })
  69. }
  70.  
  71. function initIdleListener() {
  72. // Auto refresh page on idle
  73. document.addEventListener('focus', () => {
  74. if (Date.now() - lastUpdate > TIMEOUT)
  75. setTimeout(() => refresh(), 100)
  76. lastUpdate = Date.now()
  77. })
  78. }
  79.  
  80. function getIssues() {
  81. return [...document.querySelectorAll('.notifications-list-item')]
  82. .map((el) => {
  83. const url = el.querySelector('a.notification-list-item-link').href
  84. const status = el.querySelector('.color-fg-open')
  85. ? 'open'
  86. : el.querySelector('.color-fg-done')
  87. ? 'done'
  88. : el.querySelector('.color-fg-closed')
  89. ? 'closed'
  90. : el.querySelector('.color-fg-muted')
  91. ? 'muted'
  92. : 'unknown'
  93.  
  94. const notificationTypeEl = el.querySelector('.AvatarStack').nextElementSibling
  95. const notificationType = notificationTypeEl.textContent.trim()
  96.  
  97. // Colorize notification type
  98. if (notificationType === 'mention')
  99. notificationTypeEl.classList.add('color-fg-open')
  100. else if (notificationType === 'subscribed')
  101. notificationTypeEl.classList.add('color-fg-muted')
  102. else if (notificationType === 'review requested')
  103. notificationTypeEl.classList.add('color-fg-done')
  104.  
  105. const item = {
  106. title: el.querySelector('.markdown-title').textContent.trim(),
  107. el,
  108. url,
  109. read: el.classList.contains('notification-read'),
  110. starred: el.classList.contains('notification-starred'),
  111. type: notificationType,
  112. status,
  113. isClosed: ['closed', 'done', 'muted'].includes(status),
  114. markDone: () => {
  115. console.log(`[${NAME}]`, 'Mark notifications done', item)
  116. el.querySelector('button[type=submit] .octicon-check').parentElement.parentElement.click()
  117. },
  118. }
  119.  
  120. return item
  121. })
  122. }
  123.  
  124. function autoMarkDone() {
  125. const items = getIssues()
  126.  
  127. console.log(items)
  128. let count = 0
  129.  
  130. items.forEach((i) => {
  131. // skip bookmarked notifications
  132. if (i.starred)
  133. return
  134.  
  135. // mark done for closed/merged notifications, either read or not been mentioned
  136. if (i.isClosed && (i.read || i.type === 'subscribed')) {
  137. count += 1
  138. i.markDone()
  139. }
  140.  
  141. // Renovate bot
  142. else if (i.title.startsWith('chore(deps): update ') && (i.read || i.type === 'subscribed')) {
  143. count += 1
  144. i.markDone()
  145. }
  146.  
  147. // New commit pushed to PR
  148. else if (i.url.match('/pull/[0-9]+/files/')) {
  149. count += 1
  150. i.markDone()
  151. }
  152. })
  153.  
  154. // Refresh page after marking done (expand the pagination)
  155. if (count >= 5)
  156. setTimeout(() => refresh(), 200)
  157. }
  158.  
  159. function removeBotAvatars() {
  160. document.querySelectorAll('.AvatarStack-body > a')
  161. .forEach((r) => {
  162. if (r.href.startsWith('/apps/') || r.href.startsWith('https://github.com/apps/'))
  163. r.remove()
  164. })
  165. }
  166.  
  167. // Click the notification tab to do soft refresh
  168. function refresh() {
  169. document.querySelector('.filter-list a[href="/notifications"]').click()
  170. lastUpdate = Date.now()
  171. }
  172.  
  173. ////////////////////////////////////////
  174.  
  175. let initialized = false
  176.  
  177. function run() {
  178. if (location.href.startsWith('https://github.com/notifications')) {
  179. // Run only once
  180. if (!initialized) {
  181. initIdleListener()
  182. initBroadcastChannel()
  183. initialized = true
  184. }
  185.  
  186. // Run every render
  187. dedupeTab()
  188. externalize()
  189. removeBotAvatars()
  190. autoMarkDone()
  191. }
  192. }
  193.  
  194. run()
  195.  
  196. // listen to github page loaded event
  197. document.addEventListener('pjax:end', () => run())
  198. document.addEventListener('turbo:render', () => run())
  199. })()