Collapse HackerNews Parent Comments

Adds vertical bars to the left of the comments, enabling you to easily collapse the parent comments. It also can leave only a specified number of comments expanded and auto-collapse the rest.

// ==UserScript==
// @name            Collapse HackerNews Parent Comments
// @description     Adds vertical bars to the left of the comments, enabling you to easily collapse the parent comments. It also can leave only a specified number of comments expanded and auto-collapse the rest.
// @author          BLBC (github.com/hjk789, greasyfork.org/users/679182-hjk789)
// @copyright       2020+, BLBC (github.com/hjk789, greasyfork.org/users/679182-hjk789)
// @version         1.2.6
// @homepage        https://github.com/hjk789/Userscripts/tree/master/Collapse-HackerNews-Parent-Comments
// @license         https://github.com/hjk789/Userscripts/tree/master/Collapse-HackerNews-Parent-Comments#license
// @grant           none
// @include         https://news.ycombinator.com/item*
// @namespace https://greasyfork.org/users/679182
// ==/UserScript==


//--------------- Settings -----------------

const autoCollapse = false       // Whether all comments, other than the number of comments below, should be auto-collapsed.
                                 // If set to false, all comments will be left expanded and the settings below have no effect.
    const numberOfRoots = 5
    const numberOfReplies = 3
    const numberOfRepliesOfReplies = 1

//------------------------------------------


const fadeOnHoverStyle = document.createElement("style")
fadeOnHoverStyle.innerHTML = ".verticalBar:hover { background-color: gray !important; }"
document.body.appendChild(fadeOnHoverStyle)

// HackerNews puts a 1x1 image before each comment and sets it's width according to each comment depth. Each level of depth adds 40px of width to this
// image, starting from 0 which are the root comments. The ones with a 14px width are flagged comments and the "More comments" link.
// This userscript was first created before HN implemented the root/parent/prev/next links, and at that time the layout didn't have any easier way of identifying
// the hierarchy of the comments (it was just a list of comments pushed to the right), so that's the only way I had found to achieve this at that time.

const spacingImgs = document.querySelectorAll(".ind img[height='1']:not([width='14'])")

let root = 0
let index = spacingImgs.length-1        // It's required to loop backwards, otherwise the hidden comments reappear when collapsed.
let commentHier = []

if (autoCollapse)
    var collapseAll = setInterval(function() { main(index, collapseAll) }, 1)     // An interval of 1ms is being used to prevent the page from freezing until it finishes collapsing. Also, it creates a cool effect
else                                                                              // when the comments are being collapsed. It does make it take a few more seconds to finish in comment-heavy posts (150+) though.
{
    for (let j=0; j < spacingImgs.length; j++)        // Optimize the addition of the bars when the auto collapse is disabled.
        main(j)
}



function main(i, collapseAll)
{
    let commentContainer = spacingImgs[i].parentElement.parentElement.parentElement.parentElement.parentElement.parentElement
    commentContainer.firstChild.style = "border-top: 5px transparent solid"             // To visually separate each vertical bar.
    spacingImgs[i].parentElement.style = "position: relative"

    const clicky = commentContainer.querySelectorAll(".clicky:not(.togg)")              // HN added a scrolling animation to the hierarchy links, which breaks the script.
    for (let j=0; j < clicky.length; j++)                                               // The animation is only applied to elements with the class "clicky".
        clicky[j].className = clicky[j].className.replace("clicky","")                  // This removes the clicky class from every hierarchy link of the comment.

    if (autoCollapse && !commentContainer.classList.contains("coll"))               // Collapse only if it's not collapsed yet. This is for signed-in users, as HN remembers which comments were collapsed.
        commentContainer.querySelector(".togg").click()

    index--
    i--

    if (i == -1 || i == spacingImgs.length-1)                // When finished collapsing all comments, now it's time to add the bars.
    {
        clearInterval(collapseAll)

        for (let i=0; i < spacingImgs.length; i++)
        {
            const level = spacingImgs[i].width / 40
            commentContainer = spacingImgs[i].parentElement.parentElement.parentElement.parentElement.parentElement.parentElement
            var commentToggle = commentContainer.querySelector(".togg")


            // Store the current hierarchy in an array
            commentHier[level] = commentToggle


            let divs = []
            for (let j = spacingImgs[i].width; j >= 0; j -= 40)             // Start adding the vertical bar from the current depth and go backwards.
            {
                // Create the vertical bar

                const div = document.createElement("div")
                div.className = "verticalBar"
                div.commentHier = commentHier[j/40]             // Store in an attribute of the element this comment's parent respective to the level of the vertical bar, for easy access.
                div.onclick = function(e)
                {
                    e.target.commentHier.click()                // When a vertical bar is clicked, collapse the respective parent comment.

                    // Click the "next" link of the parent comment when it's out of view.
                    if (e.target.commentHier.getBoundingClientRect().y < 0)
                        e.target.commentHier.previousElementSibling.lastChild.click()

                }

                let style = "left: " + (-5 + j) + "px; width: 12px; background-color: lightgray; position: absolute; z-index: 99; transition: 0.15s; "

                // Make it so that the vertical bars are only separated when followed by comments of same level of depth

                if (j == spacingImgs[i].width && spacingImgs[i-1] != null && spacingImgs[i].width <= spacingImgs[i-1].width)
                    style += "top: 5px; height: calc(100% + 8px); "
                else
                    style += "top: 0px; height: calc(100% + 13px); "

                div.style = style

                divs.push(div)
            }

            for (let j = divs.length - 1; j >= 0; j--)
                spacingImgs[i].parentElement.appendChild(divs[j])
        }

        if (autoCollapse)               // When finished collapsing and adding the vertical bars to all comments, now it's time to expand only a few of the first comments.
        {
            let sub40, sub80

            for (i=0; i < spacingImgs.length; i++)
            {
                commentToggle = spacingImgs[i].parentElement.parentElement.querySelector(".togg")

                if (spacingImgs[i].width == 0)          // If it's a root comment.
                {
                    root++
                    if (root == numberOfRoots + 1)          // If there's already <numberOfRoots> comments expanded, then stop expanding.
                        break

                    commentToggle.click()
                    sub40 = 0
                    sub80 = 0
                }
                else if (spacingImgs[i].width == 40 && sub40 < numberOfReplies)         // If it's a reply to the root comment, only expand up to <numberOfReplies>.
                {
                    commentToggle.click()
                    sub40++
                    sub80 = 0
                }
                else if (spacingImgs[i].width == 80 && sub80 < numberOfRepliesOfReplies)            // If it's a reply to the reply, only expand up to <numberOfRepliesOfReplies>.
                {
                    commentToggle.click()
                    sub80++
                }
            }
        }
    }
}