Refined GitHub Notifications

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

目前为 2023-04-04 提交的版本。查看 最新版本

// ==UserScript==
// @name         Refined GitHub Notifications
// @namespace    https://gf.qytechs.cn/en/scripts/461320-refined-github-notifications
// @version      0.2.4
// @description  Enhances the GitHub Notifications page, making it more productive and less noisy.
// @author       Anthony Fu (https://github.com/antfu)
// @license      MIT
// @homepageURL  https://github.com/antfu/refined-github-notifications
// @supportURL   https://github.com/antfu/refined-github-notifications
// @match        https://github.com/**
// @icon         https://www.google.com/s2/favicons?sz=64&domain=github.com
// @grant        window.close
// ==/UserScript==

/* eslint-disable no-console */

(function () {
  'use strict'

  // Fix the archive link
  if (location.pathname === '/notifications/beta/archive')
    location.pathname = '/notifications'

  const NAME = 'Refined GitHub Notifications'

  let bc
  let bcInitTime = 0

  function injectStyle() {
    const style = document.createElement('style')
    style.innerHTML = `
/* Hide blue dot on notification icon */
.mail-status.unread {
  display: none !important;
}
.js-notification-shelf {
  /* display: none !important; */
}
.btn-hover-primary {
  transform: scale(1.2);
  transition: all .3s ease-in-out;
}
.btn-hover-primary:hover {
  color: var(--color-btn-primary-text);
  background-color: var(--color-btn-primary-bg);
  border-color: var(--color-btn-primary-border);
  box-shadow: var(--color-btn-primary-shadow),var(--color-btn-primary-inset-shadow);
}
    `
    document.head.appendChild(style)
  }

  /**
   * To have a FAB button to close current issue,
   * where you can mark done and then close the tab automatically
   */
  function notificationShelf() {
    function inject() {
      const shelf = document.querySelector('.js-notification-shelf')
      if (!shelf)
        return false

      const containers = document.createElement('div')
      Object.assign(containers.style, {
        position: 'fixed',
        right: '25px',
        bottom: '25px',
        zIndex: 999,
        display: 'flex',
        flexDirection: 'column',
        gap: '10px',
      })
      document.body.appendChild(containers)

      const doneButton = shelf.querySelector('button[title="Done"]')
      // const unsubscribeButton = shelf.querySelector('button[title="Unsubscribe"]')

      const buttons = [
        // unsubscribeButton,
        doneButton,
      ].filter(Boolean)

      for (const button of buttons) {
        const clickAndClose = async () => {
          button.click()
          // wait for the notification shelf to be updated
          await new Promise((resolve) => {
            new MutationObserver(() => {
              resolve()
            })
              .observe(
                shelf,
                {
                  childList: true,
                  attributes: true,
                  subtree: true,
                  attributeFilter: ['data-redirect-to-inbox-on-submit'],
                },
              )
          })
          // close the tab
          window.close()
        }

        const fab = button.cloneNode(true)
        fab.classList.remove('btn-sm')
        fab.classList.add('btn-hover-primary')
        fab.style.aspectRatio = '1/1'
        fab.style.borderRadius = '100%'
        fab.addEventListener('click', clickAndClose)
        containers.appendChild(fab)

        if (button === doneButton) {
          document.addEventListener('keydown', (e) => {
            if ((e.metaKey || e.ctrlKey) && e.key === 'x') {
              e.preventDefault()
              clickAndClose()
            }
          })
        }
      }

      return true
    }

    // when first into the page, the notification shelf might not be loaded, we need to wait for it to show
    if (!inject()) {
      const observer = new MutationObserver((mutationList) => {
        const found = mutationList.some(i => i.type === 'childList' && Array.from(i.addedNodes).some(el => el.classList.contains('js-notification-shelf')))
        if (found) {
          inject()
          observer.disconnect()
        }
      })
      observer.observe(document.querySelector('[data-turbo-body]'), { childList: true })
    }
  }

  function initBroadcastChannel() {
    bcInitTime = Date.now()
    bc = new BroadcastChannel('refined-github-notifications')

    bc.onmessage = ({ data }) => {
      console.log(`[${NAME}]`, 'Received message', data)
      if (data.type === 'check-dedupe') {
        // If the new tab is opened after the current tab, close the current tab
        if (data.time > bcInitTime) {
          window.close()
          location.href = 'https://close-me.netlify.app'
        }
      }
    }
  }

  function dedupeTab() {
    if (!bc)
      return
    bc.postMessage({ type: 'check-dedupe', time: bcInitTime, url: location.href })
  }

  function externalize() {
    document.querySelectorAll('a')
      .forEach((r) => {
        if (r.href.startsWith('https://github.com/notifications'))
          return
        r.target = '_blank'
        r.rel = 'noopener noreferrer'
      })
  }

  function initIdleListener() {
    // Auto refresh page on going back to the page
    document.addEventListener('visibilitychange', (e) => {
      if (document.visibilityState === 'visible')
        refresh()
    })
  }

  function getIssues() {
    return [...document.querySelectorAll('.notifications-list-item')]
      .map((el) => {
        const url = el.querySelector('a.notification-list-item-link').href
        const status = el.querySelector('.color-fg-open')
          ? 'open'
          : el.querySelector('.color-fg-done')
            ? 'done'
            : el.querySelector('.color-fg-closed')
              ? 'closed'
              : el.querySelector('.color-fg-muted')
                ? 'muted'
                : 'unknown'

        const notificationTypeEl = el.querySelector('.AvatarStack').nextElementSibling
        const notificationType = notificationTypeEl.textContent.trim()

        // Colorize notification type
        if (notificationType === 'mention')
          notificationTypeEl.classList.add('color-fg-open')
        else if (notificationType === 'subscribed')
          notificationTypeEl.classList.add('color-fg-muted')
        else if (notificationType === 'review requested')
          notificationTypeEl.classList.add('color-fg-done')

        const item = {
          title: el.querySelector('.markdown-title').textContent.trim(),
          el,
          url,
          read: el.classList.contains('notification-read'),
          starred: el.classList.contains('notification-starred'),
          type: notificationType,
          status,
          isClosed: ['closed', 'done', 'muted'].includes(status),
          markDone: () => {
            console.log(`[${NAME}]`, 'Mark notifications done', item)
            el.querySelector('button[type=submit] .octicon-check').parentElement.parentElement.click()
          },
        }

        return item
      })
  }

  function getReasonMarkedDone(item) {
    if (item.isClosed && (item.read || item.type === 'subscribed'))
      return 'Closed / merged'

    if (item.title.startsWith('chore(deps): update ') && (item.read || item.type === 'subscribed'))
      return 'Renovate bot'

    if (item.url.match('/pull/[0-9]+/files/'))
      return 'New commit pushed to PR'

    if (item.type === 'ci activity' && /workflow run cancell?ed/.test(item.title))
      return 'GH PR Audit Action workflow run cancelled, probably due to another run taking precedence'
  }

  function isInboxView() {
    const query = new URLSearchParams(window.location.search).get('query')
    if (!query)
      return true

    const conditions = query.split(' ')
    return ['is:done', 'is:saved'].every(condition => !conditions.includes(condition))
  }

  function autoMarkDone() {
    // Only mark on "Inbox" view
    if (!isInboxView())
      return

    const items = getIssues()

    console.log(`[${NAME}] ${items}`)
    let count = 0

    const done = []

    items.forEach((i) => {
      // skip bookmarked notifications
      if (i.starred)
        return

      const reason = getReasonMarkedDone(i)
      if (!reason)
        return

      count++
      i.markDone()
      done.push({
        title: i.title,
        reason,
        link: i.link,
      })
    })

    if (done.length) {
      console.log(`[${NAME}]`, `${count} notifications marked done`)
      console.table(done)
    }

    // Refresh page after marking done (expand the pagination)
    if (count >= 5)
      setTimeout(() => refresh(), 200)
  }

  function removeBotAvatars() {
    document.querySelectorAll('.AvatarStack-body > a')
      .forEach((r) => {
        if (r.href.startsWith('/apps/') || r.href.startsWith('https://github.com/apps/'))
          r.remove()
      })
  }

  /**
   * The "x new notifications" badge
   */
  function hasNewNotifications() {
    return !!document.querySelector('.js-updatable-content a[href="/notifications?query="]')
  }

  // Click the notification tab to do soft refresh
  function refresh() {
    if (!isInNotificationPage())
      return
    document.querySelector('.filter-list a[href="/notifications"]').click()
  }

  function isInNotificationPage() {
    return location.href.startsWith('https://github.com/notifications')
  }

  function observeForNewNotifications() {
    try {
      const observer = new MutationObserver(() => {
        if (hasNewNotifications())
          refresh()
      })
      observer.observe(document.querySelector('.js-check-all-container').children[0], {
        childList: true,
        subtree: true,
      })
    }
    catch (e) {
    }
  }

  ////////////////////////////////////////

  let initialized = false

  function run() {
    if (isInNotificationPage()) {
      // Run only once
      if (!initialized) {
        initIdleListener()
        initBroadcastChannel()
        observeForNewNotifications()
        initialized = true
      }

      // Run every render
      dedupeTab()
      externalize()
      removeBotAvatars()
      autoMarkDone()
    }
    else {
      notificationShelf()
    }
  }

  injectStyle()
  run()

  // listen to github page loaded event
  document.addEventListener('pjax:end', () => run())
  document.addEventListener('turbo:render', () => run())
})()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址