Refined GitHub Notifications

Enhances the GitHub Notifications page, making it more productive and less noisy.

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

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