YouTube - Comment Snub

Block annoying user comments.

As of 2021-03-08. See the latest version.

// ==UserScript==
// @name                YouTube - Comment Snub
// @description         Block annoying user comments.
// @version             1.3.8
// @author              wormboy
// @namespace           patchmonkey
// @license             MIT
// @match               https://www.youtube.com/*
// @run-at              document-idle
// @grant               GM.setValue
// @grant               GM.getValue
// @noframes
// ==/UserScript==

const BLACKLIST_KEY = 'youtube-comment-snub'

async function loadBlacklist() {
  updateSnubIds(await getBlacklist())
}

function getBlacklist() {
  try {
    return GM.getValue(BLACKLIST_KEY, [])
  } catch(e) {
    return JSON.parse(localStorage[BLACKLIST_KEY] || '[]')
  }
}

function setValue(val) {
  try {
    return GM.setValue(BLACKLIST_KEY, val)
  } catch(e) {
    localStorage[BLACKLIST_KEY] = JSON.stringify(val)
  }
}

function observeComments(thread) {
  for (const child of thread.children) {
    tagComment(child)
  }
  commentObserver.observe(thread, { childList: true })
}

function tagComment(node) {
  if (node.nodeName == 'YTD-COMMENT-THREAD-RENDERER' || node.nodeName == 'YTD-COMMENT-RENDERER') {
    addButton(node)
    const target = node.querySelector('#author-thumbnail a')
    linkMap.set(target, node)
    linkObserver.observe(target, { attributeFilter: ['href'] })
    updateCommentTag(node, target)
  }
}

function updateCommentTag(node, target) {
  node.dataset.snub = target.getAttribute('href')
}

async function quarantineUser(event) {
  const { snub } = buttonMap.get(event.currentTarget).dataset
  let list = await getBlacklist()
  list = new Set(list)
  list.add(snub)
  list = Array.from(list)
  updateSnubIds(list)
  await setValue(list)
}

const createCssRule = rules => rules.length ? `${rules} { display: none !important; }` : ''

function updateSnubIds(list) {
  let style = document.head.querySelector('#snub-id-list')
  if (!style) {
    style = document.head.appendChild(document.createElement('style'))
    style.id = 'snub-id-list'
  }
  style.textContent = createCssRule(list.map(i => `[data-snub="${i}"]`).join(',\n'))
}

function waitForToolbar(node) {
  return new Promise(resolve => {
    const toolbar = node.querySelector('#toolbar')
    if (toolbar) resolve(toolbar)
    else {
      const observer = new MutationObserver(() => {
        const el = node.querySelector('#toolbar')
        if (el) {
          observer.disconnect()
          resolve(el)
        }
      })
      observer.observe(node, { childList: true, subtree: true })
    }
  })
}

const iconElement = document.createElement('div')
iconElement.innerHTML = `
  <button id="button" class="style-scope yt-icon-button" aria-label="snub this user'" title="Snub">
    <div id="icon" class="style-scope ytd-toggle-button-renderer">
      <div style="pointer-events: none;">🤐</div>
    </div>
  </button>
`
iconElement.querySelector('#button').style.cssText = `
  padding: var(--yt-button-icon-padding, 8px);
  width: var(--yt-button-icon-size, var(--yt-icon-width, 40px));
  height: var(--yt-button-icon-size, var(--yt-icon-height, 40px));
`
iconElement.style.cssText = `
  --yt-button-icon-size: var(--ytd-comment-thumb-dimension);
  margin: 0 8px 0 -8px;
`
iconElement.id = 'snub-button'

function addButton(node) {
  waitForToolbar(node).then(toolbar => {
    if (toolbar.querySelector('#snub-button')) return
    const el = iconElement.cloneNode(true)
    el.addEventListener('click', quarantineUser)
    if (toolbar.firstElementChild) {
      toolbar.insertBefore(el, toolbar.firstElementChild)
    } else {
      toolbar.appendChild(el)
    }
    buttonMap.set(el, node)
  })
}

const linkMap = new WeakMap()
const buttonMap = new WeakMap()

const linkObserver = new MutationObserver(records => {
  for (const { target } of records) {
    updateCommentTag(linkMap.get(target), target)
  }
})

const commentObserver = new MutationObserver(records => {
  for (const { addedNodes } of records) {
    for (const node of addedNodes) {
      tagComment(node)
    }
  }
})

const SECTION_SELECTOR = 'ytd-item-section-renderer'
const COMMENT_SELECTOR = 'ytd-comment-replies-renderer,ytd-backstage-comments-renderer'

window.addEventListener('yt-next-continuation-data-updated', ({ target: e }) => {
  if (e.matches(SECTION_SELECTOR)) {
    observeComments(e.querySelector('#contents'))
  } else if (e.matches(COMMENT_SELECTOR)) {
    observeComments(e.querySelector('#loaded-comments,#loaded-replies'))
  }
})

window.addEventListener('yt-navigate-start', loadBlacklist)

loadBlacklist()
长期地址
遇到问题?请前往 GitHub 提 Issues。