Refined GitHub Notifications

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

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

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