YouTube-Comment-Snub

Block annoying user comments.

Fra og med 16.02.2021. Se den nyeste version.

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

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

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

function setValue(val) {
  try {
    return GM.setValue('blacklist', val)
  } catch(e) {
    localStorage.blacklist = 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)
}

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'
  }
  const rules = list.length ? list.map(i => `[data-snub="${i}"]`).join(',\n') : ''
  style.textContent = rules && snubSelector(rules)
}

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 })
    }
  })
}

function addButton(node) {
  if (node.querySelector('#snub-button')) return
  waitForToolbar(node).then(toolbar => {
    const el = document.createElement('div')
    el.id = 'snub-button'
    el.style.cssText = buttonCss
    el.innerHTML = buttonTemplate('snub this user', 'Snub')
    el.querySelector('#button').style.cssText = iconButtonCss
    el.addEventListener('click', quarantineUser)
    const icon = el.querySelector('#icon')
    icon.innerHTML = snubIcon
    if (toolbar.firstElementChild) {
      toolbar.insertBefore(el, toolbar.firstElementChild)
    } else {
      toolbar.appendChild(el)
    }
    buttonMap.set(el, node)
  })
}

function reusable(strs) {
  return function(...vals) {
    return strs.map((s, i) => `${s}${vals[i] || ''}`).join('')
  }
}

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 buttonTemplate = reusable`
  <button id="button" class="style-scope yt-icon-button" aria-label="${'aria'}">
    <div id="icon" class="style-scope ytd-toggle-button-renderer"></div>
  </button>
  <paper-tooltip>${'tooltip'}</paper-tooltip>
`

const buttonCss = `
  --yt-button-icon-size: var(--ytd-comment-thumb-dimension);
  margin: 0 8px 0 -8px;
`

const iconButtonCss = `
  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));
`

const snubIcon = `
  <div style="pointer-events: none;">🤐</div>
`

const snubSelector = reusable`
  ${'list'} {
    display: none !important;
  }
`

window.addEventListener('yt-next-continuation-data-updated', ({ target: e }) => {
  observeComments(e.querySelector('#contents'))
})

window.addEventListener('yt-next-continuation-data-updated', ({ target: e }) => {
  if (e.matches('ytd-comment-replies-renderer,ytd-backstage-comments-renderer')) {
    observeComments(e.querySelector('#loaded-comments,#loaded-replies'))
  }
})

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

loadBlacklist()