YouTube Comment Snub

Hide comments by specific users in the comments section.

// ==UserScript==
// @name                YouTube Comment Snub
// @description         Hide comments by specific users in the comments section.
// @version             1.4.3
// @author              stinkrock
// @namespace           patchmonkey
// @license             MIT
// @match               https://www.youtube.com/*
// @run-at              document-idle
// @noframes
// ==/UserScript==


const conf = { id: 'snub-button', innerText: '🤐', ariaLabel: 'Snub this user' }

const style = `cursor:pointer;font-size:14px;margin:0 8px 0 -8px;padding:var(--yt-button-icon-padding,8px);`


const YTNEXT = 'yt-next-continuation-data-updated'

const YTNAVIGATE = 'yt-navigate-start'


const YTDSECTION = 'ytd-comments ytd-item-section-renderer'

const YTDREPLIES = 'ytd-comments ytd-comment-replies-renderer'

const YTDTHREAD = 'ytd-comment-thread-renderer'

const YTDCOMMENT = 'ytd-comment-renderer'


const CONTENTS = '#contents'

const TOOLBAR = '#toolbar'

const AUTHOR = '#author-thumbnail a'

const SNUB = '#' + conf.id


const key = 'youtube-comment-snub'

const users = new Set()

const containers = new WeakSet()

const buttons = new WeakMap()

const thumbs = new WeakMap()


const comp = (f, g) => x => g(f(x))

const compose = (...f) => f.reduce(comp)


const or = (f, g) => x => f(x) || g(x)

const either = (...f) => f.reduce(or)


const cond = (f, g) => x => Promise.resolve(x).then(f).catch(g)

const condition = (f, g) => x => f(x) ? g(x) : null


const each = x => f => f(x)

const list = (...f) => x => f.map(each(x))

const foreach = (...f) => x => f.forEach(each(x))

const flatmap = (...f) => x => f.flatMap(each(x))

const filter = f => x => x.filter(f)

const map = f => x => x.map(f)

const flat = x => x.flat()

const join = x => y => y.join(x)


const id = x => x

const opt = x => y => y != null ? y : x

const getitem = (x, y) => () => x.getItem(y)

const setitem = (x, y) => z => x.setItem(y, z)

const stringify = x => JSON.stringify(x)

const parse = x => JSON.parse(x)


const set = x => ([...y]) => x.set(...y)

const get = x => y => x.get(y)

const add = x => y => x.add(y)

const has = x => y => x.has(y)

const clear = x => x.clear()


const observer = f => new MutationObserver(f)

const childlist = x => y => x.observe(y, { childList: true })

const attribute = x => y => x.observe(y, { attributeFilter: ['href'] })

const subtree = x => y => x.observe(y, { childList: true, subtree: true })


const elem = x => document.createElement(x)

const text = x => document.createTextNode(x)

const append = (x, y) => x.appendChild(y)

const insert = ([x, y, z]) => x.insertBefore(y, z)

const match = x => y => y.matches(x) ? y : null

const query = x => y => y.querySelector(x)

const clone = x => y => x.cloneNode(y)

const attr = x => y => y.getAttribute(x)

const div = () => elem('div')


const children = x => [...x.children]

const added = x => [...x.addedNodes]

const first = x => x.firstElementChild

const current = x => x.currentTarget

const target = x => x.target

const type = x => x.nodeType

const number = x => isNaN(x) ? 0 : x

const lower = x => x.toLowerCase()


const array = x => [...x]

const value = x => () => x

const equal = x => y => x == y

const assign = x => y => Object.assign(y, x)


const data = x => y => x.data = y

const rule = x => `[data-snub="${x}"]`

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

const css = x => y => (y.style.cssText = x, y)

const snub = ([x, y]) => x.dataset.snub = y


const sheet = append(document.documentElement, elem('style'))

const rules = append(sheet, text(''))

const compile = compose(map(rule), join(',\n'), body)


const read = compose(getitem(localStorage, key), opt('[]'), parse)

const write = compose(stringify, setitem(localStorage, key))


const update = compose(compile, data(rules))

const load = compose(read, foreach(map(add(users)), update))

const reset = compose(value(users), clear, load)

const save = compose(value(users), array, foreach(write, update))


const tag = compose(list(id, compose(query(AUTHOR), attr('href'))), snub)

const link = compose(list(get(thumbs), attr('href')), snub)

const change = compose(map(target), flat, map(link))

const observe = compose(query(AUTHOR), attribute(observer(change)))


const quarantine = compose(get(buttons), attr('href'), add(users))

const onclick = compose(current, foreach(quarantine, save))

const button = compose(div, assign(conf), assign({ onclick }), css(style))


const action = compose(query(TOOLBAR), list(id, button, first), insert)

const auth = compose(list(query(SNUB), query(AUTHOR)), set(buttons))

const tool = compose(list(query(AUTHOR), id), set(thumbs))

const configure = foreach(action, auth, tool)


const matches = flatmap(filter(match(YTDTHREAD)), filter(match(YTDCOMMENT)))

const elements = filter(compose(type, equal(Node.ELEMENT_NODE)))

const content = condition(match(YTDSECTION), query(CONTENTS))

const replies = condition(match(YTDREPLIES), query(CONTENTS))


const process = foreach(configure, tag, observe)

const track = condition(compose(query(SNUB), equal(null)), process)

const all = compose(matches, map(track))

const init = foreach(add(containers), compose(children, all))

const contain = condition(compose(has(containers), equal(false)), init)


const mutations = compose(map(added), flat, elements, all)

const follow = foreach(contain, childlist(observer(mutations)))

const capture = compose(either(content, replies), follow)

const run = compose(query(YTDSECTION), query(CONTENTS), follow)


window.addEventListener(YTNEXT, compose(target, cond(capture, e => e)))

window.addEventListener(YTNAVIGATE, reset)


reset()


Promise.resolve(document.body).then(run).catch(e => e)