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.3
  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 isInboxView() {
  142. const query = new URLSearchParams(window.location.search).get('query')
  143. if (!query)
  144. return true
  145.  
  146. const conditions = query.split(' ')
  147. return ['is:done', 'is:saved'].every(condition => !conditions.includes(condition))
  148. }
  149.  
  150. function autoMarkDone() {
  151. // Only mark on "Inbox" view
  152. if (!isInboxView())
  153. return
  154.  
  155. const items = getIssues()
  156.  
  157. console.log(items)
  158. let count = 0
  159.  
  160. const done = []
  161.  
  162. items.forEach((i) => {
  163. // skip bookmarked notifications
  164. if (i.starred)
  165. return
  166.  
  167. const reason = getReasonMarkedDone(i)
  168. if (!reason)
  169. return
  170.  
  171. count++
  172. i.markDone()
  173. done.push({
  174. title: i.title,
  175. reason,
  176. link: i.link,
  177. })
  178. })
  179.  
  180. if (done.length) {
  181. console.log(`[${NAME}]`, `${count} notifications marked done`)
  182. console.table(done)
  183. }
  184.  
  185. // Refresh page after marking done (expand the pagination)
  186. if (count >= 5)
  187. setTimeout(() => refresh(), 200)
  188. }
  189.  
  190. function removeBotAvatars() {
  191. document.querySelectorAll('.AvatarStack-body > a')
  192. .forEach((r) => {
  193. if (r.href.startsWith('/apps/') || r.href.startsWith('https://github.com/apps/'))
  194. r.remove()
  195. })
  196. }
  197.  
  198. // Click the notification tab to do soft refresh
  199. function refresh() {
  200. document.querySelector('.filter-list a[href="/notifications"]').click()
  201. lastUpdate = Date.now()
  202. }
  203.  
  204. ////////////////////////////////////////
  205.  
  206. let initialized = false
  207.  
  208. function run() {
  209. if (location.href.startsWith('https://github.com/notifications')) {
  210. // Run only once
  211. if (!initialized) {
  212. initIdleListener()
  213. initBroadcastChannel()
  214. initialized = true
  215. }
  216.  
  217. // Run every render
  218. dedupeTab()
  219. externalize()
  220. removeBotAvatars()
  221. autoMarkDone()
  222. }
  223. }
  224.  
  225. run()
  226.  
  227. // listen to github page loaded event
  228. document.addEventListener('pjax:end', () => run())
  229. document.addEventListener('turbo:render', () => run())
  230. })()