YouTube Comment Pin

Move comments by specific users to the top of the comments section.

// ==UserScript==
// @name                YouTube Comment Pin
// @description         Move comments by specific users to the top of the comments section.
// @version             1.2.4
// @author              stinkrock
// @license             MIT
// @namespace           patchmonkey
// @match               https://www.youtube.com/*
// @run-at              document-idle
// @noframes
// ==/UserScript==


const conf = { id: 'pin-button', innerHTML: `<div>📌</div>`, ariaLabel: 'Pin 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 YTDTHREAD = 'ytd-comment-thread-renderer'


const CONTENTS = '#contents'

const TOOLBAR = '#toolbar'

const AUTHOR = '#author-thumbnail a'

const BUTTON = '#' + conf.id


const PIN = 'pinned'

const KEY = 'youtube-pin-comments'


const tree = `<div><slot name="${PIN}"></slot></div><slot></slot>`

const rot = `${YTDTHREAD}[slot="${PIN}"] ${BUTTON} div { transform: rotate(-45deg); }`


const users = new Set()

const comments = 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 tern = (f, g, h) => x => f(x) ? g(x) : h(x)


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 del = x => y => x.delete(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 div = () => elem('div')


const shadow = x => x.attachShadow({ mode: 'open' })

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

const html = x => y => y.innerHTML = x

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

const slot = ([x, y]) => x.slot = y

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


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

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

const removed = x => [...x.removedNodes]

const first = x => x.firstElementChild

const current = x => x.currentTarget

const target = x => x.target

const type = x => x.nodeType


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

const value = x => () => x

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

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


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

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

const layout = data(rules)


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

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


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

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

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


const name = compose(attr('href'), tern(has(users), value(PIN), value('')))

const pin = compose(list(get(thumbs), name), slot)

const tag = compose(query(AUTHOR), pin)


const toggle = compose(attr('href'), tern(has(users), del(users), add(users)))

const rehash = compose(value(comments), array, map(tag))

const onclick = compose(current, get(buttons), toggle, rehash, 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(BUTTON), query(AUTHOR)), set(buttons))

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

const configure = foreach(action, auth, tool)


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

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


const matches = filter(match(YTDTHREAD))

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

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


const process = foreach(configure, tag, observe)

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

const watch = compose(matches, map(foreach(track, add(comments))))

const ignore = compose(matches, map(del(comments)))


const scope = compose(shadow, html(tree))

const init = foreach(add(containers), scope, compose(children, watch))

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


const keep = compose(map(added), flat, elements, watch)

const discard = compose(map(removed), flat, elements, ignore)

const mutations = foreach(keep, discard)


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

const capture = compose(content, follow)

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


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

window.addEventListener(YTNAVIGATE, reset)


layout(rot)

reset()


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