HackerBrain - sidebar for efficient information consumption

Read Hacker News top-level comments in right-sidebar with truncation support and persistence.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         HackerBrain - sidebar for efficient information consumption
// @namespace    https://news.ycombinator.com/
// @version      3.16
// @description  Read Hacker News top-level comments in right-sidebar with truncation support and persistence.
// @match        https://news.ycombinator.com/*
// @grant        GM_xmlhttpRequest
// @connect      news.ycombinator.com
// @license      MIT
// author        adrianwaj (HN)
// ==/UserScript==

(function () {
    "use strict";

    if (location.pathname === "/item") return;

    // --- Configuration & Constants ---
    const CONFIG = {
        STORAGE_KEY_VISITED: "hn-sidebar-visited-items",
        STORAGE_KEY_WIDTH: "hn-sidebar-width",
        STORAGE_KEY_TRUNCATED: "hn-sidebar-truncated-comments",
        STORAGE_KEY_THREAD_DATA: "hn-sidebar-thread-data",
        DEFAULT_WIDTH: 360,
        MIN_WIDTH: 200,
        MAX_WIDTH: 800,
        MAX_STORED_ITEMS: 5000,
        TRUNCATE_TTL_MS: 90 * 24 * 60 * 60 * 1000, // 90 days in milliseconds
    };

    let activeSidebarLink = null;
    let activeTruncateBtn = null;
    let lastSidebarItem = null;
    let currentThreadRoots = []; // Local in-memory store for active item's root comments

    // --- State Caching ---
    let cachedVisited = null;
    let cachedTruncated = null;
    let cachedThreadData = null;

    // --- LocalStorage Helpers with Auto-Pruning ---
    const getVisitedItems = () => {
        if (!cachedVisited) {
            try {
                cachedVisited = JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEY_VISITED)) || [];
            } catch {
                cachedVisited = [];
            }
        }
        return cachedVisited;
    };

    const markItemAsVisited = (itemId) => {
        const visited = getVisitedItems();
        if (!visited.includes(itemId)) {
            visited.push(itemId);
            while (visited.length > CONFIG.MAX_STORED_ITEMS) {
                visited.shift();
            }
            try {
                localStorage.setItem(CONFIG.STORAGE_KEY_VISITED, JSON.stringify(visited));
            } catch (e) {
                console.warn("HN Sidebar: Failed to save visited state", e);
            }
        }
    };

    const getTruncatedComments = () => {
        if (!cachedTruncated) {
            try {
                const raw = JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEY_TRUNCATED)) || [];
                const now = Date.now();

                // Migrate legacy string/number arrays & prune expired timestamps (>90 days)
                cachedTruncated = raw
                    .map((item) => (typeof item === "object" && item !== null ? item : { id: String(item), ts: now }))
                    .filter((item) => now - item.ts < CONFIG.TRUNCATE_TTL_MS);
            } catch {
                cachedTruncated = [];
            }
        }
        return cachedTruncated;
    };

    const getTruncatedCommentIds = () => {
        return getTruncatedComments().map((item) => item.id);
    };

    const markCommentAsTruncated = (commentId) => {
        let truncated = getTruncatedComments();
        const now = Date.now();
        const strId = String(commentId);

        truncated = truncated.filter((item) => now - item.ts < CONFIG.TRUNCATE_TTL_MS);

        if (!truncated.some((item) => item.id === strId)) {
            truncated.push({ id: strId, ts: now });
            while (truncated.length > CONFIG.MAX_STORED_ITEMS) {
                truncated.shift();
            }
            cachedTruncated = truncated;
            try {
                localStorage.setItem(CONFIG.STORAGE_KEY_TRUNCATED, JSON.stringify(truncated));
            } catch (e) {
                console.warn("HN Sidebar: Failed to save truncated comment state", e);
            }
        }
    };

    const getThreadData = () => {
        if (!cachedThreadData) {
            try {
                cachedThreadData = JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEY_THREAD_DATA)) || {};
            } catch {
                cachedThreadData = {};
            }
        }
        return cachedThreadData;
    };

    const saveThreadData = (itemId, roots) => {
        const threadData = getThreadData();
        const topCommentIds = roots.map((r) => r.id);

        threadData[itemId] = {
            totalTop: roots.length,
            topCommentIds: topCommentIds,
        };

        const keys = Object.keys(threadData);
        if (keys.length > CONFIG.MAX_STORED_ITEMS) {
            delete threadData[keys[0]];
        }

        try {
            localStorage.setItem(CONFIG.STORAGE_KEY_THREAD_DATA, JSON.stringify(threadData));
        } catch (e) {
            console.warn("HN Sidebar: Failed to save thread data", e);
        }
    };

    // --- DOM Sanitizer returning DocumentFragment ---
    const sanitizeToFragment = (rawHtml) => {
        const fragment = document.createDocumentFragment();
        if (!rawHtml) return fragment;

        const parser = new DOMParser();
        const doc = parser.parseFromString(rawHtml, "text/html");

        const allowedTags = new Set([
            "A", "P", "B", "I", "U", "EM", "STRONG", "CODE", "PRE",
            "SPAN", "DIV", "BR", "BLOCKQUOTE", "SUB", "SUP"
        ]);

        const cleanNode = (node) => {
            const children = Array.from(node.childNodes);

            for (const child of children) {
                if (child.nodeType === Node.ELEMENT_NODE) {
                    const tagName = child.tagName.toUpperCase();

                    if (!allowedTags.has(tagName)) {
                        child.remove();
                        continue;
                    }

                    const attrs = Array.from(child.attributes);
                    for (const attr of attrs) {
                        const attrName = attr.name.toLowerCase();
                        const attrVal = attr.value.trim().toLowerCase();

                        if (attrName.startsWith("on") ||
                            ((attrName === "href" || attrName === "src") &&
                             (attrVal.startsWith("javascript:") || attrVal.startsWith("data:")))) {
                            child.removeAttribute(attr.name);
                        }
                    }

                    if (tagName === "A") {
                        child.setAttribute("rel", "noopener noreferrer");
                        child.setAttribute("target", "_blank");
                    }

                    cleanNode(child);
                }
            }
        };

        cleanNode(doc.body);

        while (doc.body.firstChild) {
            fragment.appendChild(doc.body.firstChild);
        }

        return fragment;
    };

    // --- Inject CSS Styles ---
    const style = document.createElement("style");
    style.textContent = `
        #hn-comment-sidebar,
        #hn-sidebar-content {
            cursor: crosshair;
        }

        #hn-comment-sidebar {
            position: fixed;
            right: 0;
            top: 0;
            width: ${localStorage.getItem(CONFIG.STORAGE_KEY_WIDTH) || CONFIG.DEFAULT_WIDTH}px;
            height: 100vh;
            overflow-y: auto;
            background: #f6f6ef;
            padding: 12px;
            z-index: 99999;
            font-family: Verdana, Geneva, sans-serif;
            font-size: 10pt;
            border-left: 1px solid #828282;
            box-sizing: border-box;
            color: #000000;
        }

        #hn-sidebar-resize {
            position: absolute;
            left: -4px;
            top: 0;
            width: 8px;
            height: 100%;
            cursor: ew-resize;
        }

        #hn-sidebar-close {
            margin-top: 16px;
            padding-bottom: 12px;
            cursor: default;
        }

        #hn-sidebar-x {
            color: inherit;
            text-decoration: none;
            font-weight: bold;
            cursor: pointer;
        }

        .hn-sidebar-comment {
            margin-bottom: 8px;
            padding: 6px;
            background: #f6f6ef;
            cursor: default;
        }

        .hn-sidebar-comment .reply {
            display: none !important;
        }

        .hn-sidebar-user {
            color: #828282;
            font-size: 8pt;
            margin-bottom: 4px;
            cursor: default;
        }

        .hn-sidebar-user-link {
            color: #828282;
            text-decoration: none;
            cursor: pointer;
        }

        .hn-sidebar-text {
            color: inherit;
            line-height: 1.4;
            cursor: auto;
        }

        .hn-sidebar-text a {
            color: #0000aa;
            text-decoration: underline;
            cursor: pointer;
        }

        .hn-sidebar-count {
            color: #828282;
            cursor: pointer;
            margin-left: 5px;
            user-select: none;
            font-weight: bold;
        }

        .hn-sidebar-replies {
            display: none;
            margin-left: 14px;
            margin-top: 8px;
            border-left: 2px solid #e0e0d8;
            padding-left: 6px;
        }

        .hn-sidebar-comment.open > .hn-sidebar-replies {
            display: block;
        }

        .hn-sidebar-link {
            color: #828282;
            text-decoration: none;
            margin-left: 4px;
            padding: 0 3px;
            border-radius: 2px;
        }

        .hn-sidebar-link.active-open {
            background: #ff6600;
            color: #ffffff !important;
            opacity: 1;
        }

        .hn-sidebar-link.active-closed {
            background: #606060;
            color: #ffffff !important;
            opacity: 0.4;
        }

        .hn-sidebar-truncate-btn {
            background: #a7dca5;
            color: #222222 !important;
            padding: 0 3px;
            margin-left: 4px;
            text-decoration: none;
            border-radius: 2px;
            cursor: pointer;
            user-select: none;
            opacity: 1;
            pointer-events: auto;
        }

        /* Transparent background when count is 0 */
        .hn-sidebar-truncate-btn.zero-count {
            background: transparent !important;
            color: #828282 !important;
            opacity: 1;
        }

        /* Grayed out & unclickable when dimmed (if not 0) */
        .hn-sidebar-truncate-btn.dimmed {
            background: #828282 !important;
            color: #ffffff !important;
            opacity: 0.5;
            cursor: default;
            pointer-events: none;
        }

        .hn-sidebar-truncate-btn.dimmed.zero-count {
            background: transparent !important;
            color: #828282 !important;
            opacity: 0.5;
        }

        .hn-sidebar-loading, .hn-sidebar-error {
            color: #828282;
            font-style: italic;
            padding: 10px 0;
            cursor: default;
        }

        .hn-sidebar-comment.hidden-comment {
            display: none !important;
        }

        @media (prefers-color-scheme: dark) {
            #hn-comment-sidebar {
                background: #1a1a1a;
                border-left-color: #444444;
                color: #cccccc;
            }

            .hn-sidebar-comment {
                background: #1a1a1a;
            }

            .hn-sidebar-text a {
                color: #63a0ff;
            }

            .hn-sidebar-replies {
                border-left-color: #333333;
            }

            .hn-sidebar-truncate-btn {
                background: #2e5c2d;
                color: #e0e0e0 !important;
            }

            .hn-sidebar-truncate-btn.zero-count {
                background: transparent !important;
                color: #828282 !important;
            }

            .hn-sidebar-truncate-btn.dimmed {
                background: #555555 !important;
                color: #aaaaaa !important;
                opacity: 0.4;
            }

            .hn-sidebar-truncate-btn.dimmed.zero-count {
                background: transparent !important;
                color: #555555 !important;
                opacity: 0.4;
            }

            .hn-sidebar-link.active-closed {
                background: #2c2c2c;
                color: #666666 !important;
                opacity: 0.4;
            }
        }
    `;
    document.head.appendChild(style);

    // --- Build UI ---
    const sidebar = document.createElement("div");
    sidebar.id = "hn-comment-sidebar";
    sidebar.style.display = "none";

    const resizeHandle = document.createElement("div");
    resizeHandle.id = "hn-sidebar-resize";

    const sidebarContent = document.createElement("div");
    sidebarContent.id = "hn-sidebar-content";

    const closeContainer = document.createElement("div");
    closeContainer.id = "hn-sidebar-close";

    const closeBtn = document.createElement("a");
    closeBtn.id = "hn-sidebar-x";
    closeBtn.href = "#";
    closeBtn.textContent = "x";
    closeContainer.appendChild(closeBtn);

    sidebar.appendChild(resizeHandle);
    sidebar.appendChild(sidebarContent);
    sidebar.appendChild(closeContainer);
    document.body.appendChild(sidebar);

    // Delegated click listener for toggling reply counters
    sidebarContent.addEventListener("click", (e) => {
        if (e.target && e.target.classList.contains("hn-sidebar-count")) {
            e.stopPropagation();
            const commentNode = e.target.closest(".hn-sidebar-comment");
            if (commentNode) {
                commentNode.classList.toggle("open");
            }
        }
    });

    // --- Optimized Resizing Logic ---
    let isResizing = false;
    let resizeAnimationFrame = null;

    const onMouseMove = (e) => {
        if (!isResizing) return;

        if (resizeAnimationFrame) cancelAnimationFrame(resizeAnimationFrame);

        resizeAnimationFrame = requestAnimationFrame(() => {
            const newWidth = window.innerWidth - e.clientX;
            if (newWidth >= CONFIG.MIN_WIDTH && newWidth <= CONFIG.MAX_WIDTH) {
                sidebar.style.width = `${newWidth}px`;
            }
        });
    };

    const onMouseUp = () => {
        if (!isResizing) return;
        isResizing = false;
        document.body.style.userSelect = "";

        localStorage.setItem(CONFIG.STORAGE_KEY_WIDTH, parseInt(sidebar.offsetWidth, 10));

        document.removeEventListener("mousemove", onMouseMove);
        document.removeEventListener("mouseup", onMouseUp);
    };

    resizeHandle.addEventListener("mousedown", (e) => {
        e.preventDefault();
        isResizing = true;
        document.body.style.userSelect = "none";

        document.addEventListener("mousemove", onMouseMove);
        document.addEventListener("mouseup", onMouseUp);
    });

    // --- Counter Handlers ---
    const updateCounts = () => {
        if (!sidebarContent) return;
        const totalTopComments = currentThreadRoots.length;
        const visibleTopComments = currentThreadRoots.filter((comment) => !comment.truncated).length;

        if (activeSidebarLink) {
            activeSidebarLink.textContent = totalTopComments;
        }
        if (activeTruncateBtn) {
            activeTruncateBtn.textContent = visibleTopComments;
            if (visibleTopComments === 0) {
                activeTruncateBtn.classList.add("zero-count");
            } else {
                activeTruncateBtn.classList.remove("zero-count");
            }
        }
    };

    const closeSidebar = () => {
        sidebar.style.display = "none";

        if (activeTruncateBtn) {
            activeTruncateBtn.classList.add("dimmed");
        }

        if (activeSidebarLink) {
            activeSidebarLink.classList.remove("active-open");
            activeSidebarLink.classList.add("active-closed");
        }
    };

    closeBtn.addEventListener("click", (e) => {
        e.preventDefault();
        closeSidebar();
    });

    sidebar.addEventListener("click", (e) => {
        if (e.target === sidebar || e.target === sidebarContent) {
            closeSidebar();
        }
    });

    sidebar.addEventListener("contextmenu", (e) => {
        if (e.target === sidebar || e.target === sidebarContent) {
            e.preventDefault();
            closeSidebar();
        }
    });

    const truncateTopComment = () => {
        // Find first untruncated comment from memory state
        const targetComment = currentThreadRoots.find((comment) => !comment.truncated);
        if (targetComment) {
            // Update flag locally
            targetComment.truncated = true;
            markCommentAsTruncated(targetComment.id);

            // Reflect state change in DOM
            const element = sidebarContent.querySelector(`:scope > .hn-sidebar-comment[data-comment-id="${targetComment.id}"]`);
            if (element) {
                element.classList.add("hidden-comment");
            }

            sidebar.scrollTop = 0;
            updateCounts();
        }
    };

    // Keyboard Hotkeys
    document.addEventListener("keydown", (e) => {
        if (sidebar.style.display === "none") return;

        const activeTag = document.activeElement ? document.activeElement.tagName.toUpperCase() : "";
        if (activeTag === "INPUT" || activeTag === "TEXTAREA" || document.activeElement.isContentEditable) {
            return;
        }

        if (e.key === "Escape") {
            closeSidebar();
        } else if (e.code === "Space") {
            e.preventDefault();
            truncateTopComment();
        }
    });

    const setupTruncateButton = (parentElement) => {
        let btn = parentElement.querySelector(".hn-sidebar-truncate-btn");
        if (!btn) {
            btn = document.createElement("a");
            btn.href = "#";
            btn.textContent = "...";
            btn.className = "hn-sidebar-truncate-btn dimmed";
            btn.title = "Hide top visible comment (Spacebar)";

            btn.addEventListener("click", (e) => {
                e.preventDefault();
                truncateTopComment();
            });

            parentElement.appendChild(btn);
        }

        activeTruncateBtn = btn;
    };

    // --- Network & Parsing ---
    const fetchItemHtml = (id) => {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `https://news.ycombinator.com/item?id=${id}`,
                onload: (res) => {
                    if (res.status >= 200 && res.status < 400) {
                        resolve(res.responseText);
                    } else {
                        reject(new Error(`HTTP ${res.status}`));
                    }
                },
                onerror: reject,
            });
        });
    };

    const parseComments = (html) => {
        const doc = new DOMParser().parseFromString(html, "text/html");
        const comments = [];
        const truncatedIds = getTruncatedCommentIds();

        doc.querySelectorAll(".athing.comtr").forEach((row) => {
            const indentImg = row.querySelector(".ind img");
            const widthAttr = indentImg ? parseInt(indentImg.getAttribute("width") || "0", 10) : 0;
            const level = Math.floor(widthAttr / 40);
            const commentId = row.id.replace("comment_", "");
            const rawCommentHtml = row.querySelector(".comment")?.innerHTML || "";

            comments.push({
                id: commentId,
                level,
                user: row.querySelector(".hnuser")?.textContent || "deleted",
                userUrl: `https://news.ycombinator.com/item?id=${commentId}`,
                text: rawCommentHtml,
                replies: [],
            });
        });

        const roots = [];
        const stack = [];

        comments.forEach((comment) => {
            while (stack.length && stack[stack.length - 1].level >= comment.level) {
                stack.pop();
            }

            if (stack.length) {
                stack[stack.length - 1].replies.push(comment);
            } else {
                // Attach explicit truncation status flag on root-level items
                comment.truncated = truncatedIds.includes(comment.id);
                roots.push(comment);
            }

            stack.push(comment);
        });

        return roots;
    };

    const renderComment = (comment) => {
        const element = document.createElement("div");
        element.className = "hn-sidebar-comment";
        element.dataset.commentId = comment.id;

        // Render visibility based directly on item's truncated flag state
        if (comment.truncated) {
            element.classList.add("hidden-comment");
        }

        const userDiv = document.createElement("div");
        userDiv.className = "hn-sidebar-user";

        const userLink = document.createElement("a");
        userLink.className = "hn-sidebar-user-link";
        userLink.href = comment.userUrl;
        userLink.target = "_blank";
        userLink.rel = "noopener noreferrer";
        userLink.textContent = comment.user;
        userDiv.appendChild(userLink);

        const count = comment.replies.length;
        if (count > 0) {
            const countSpan = document.createElement("span");
            countSpan.className = "hn-sidebar-count";
            countSpan.textContent = ` [${count}]`;
            userDiv.appendChild(countSpan);
        }

        const textDiv = document.createElement("div");
        textDiv.className = "hn-sidebar-text";
        textDiv.appendChild(sanitizeToFragment(comment.text));

        const repliesContainer = document.createElement("div");
        repliesContainer.className = "hn-sidebar-replies";

        if (count > 0) {
            const repliesFragment = document.createDocumentFragment();
            comment.replies.forEach((reply) => {
                repliesFragment.appendChild(renderComment(reply));
            });
            repliesContainer.appendChild(repliesFragment);
        }

        element.appendChild(userDiv);
        element.appendChild(textDiv);
        element.appendChild(repliesContainer);

        return element;
    };

    // --- Sidebar Actions ---
    const openSidebar = async (itemId, triggerLink) => {
        sidebar.style.display = "block";
        markItemAsVisited(itemId);

        // Dim button of the previously active item if changing items
        if (activeTruncateBtn && activeSidebarLink !== triggerLink) {
            activeTruncateBtn.classList.add("dimmed");
        }

        if (activeSidebarLink && activeSidebarLink !== triggerLink) {
            activeSidebarLink.classList.remove("active-open");
            activeSidebarLink.classList.add("active-closed");
        }

        activeSidebarLink = triggerLink;
        activeSidebarLink.classList.remove("active-closed");
        activeSidebarLink.classList.add("active-open");

        // Setup button for this item's container & remove dimmed state for active item
        setupTruncateButton(triggerLink.parentNode);
        activeTruncateBtn.classList.remove("dimmed");

        lastSidebarItem = itemId;

        sidebarContent.replaceChildren();
        const loadingDiv = document.createElement("div");
        loadingDiv.className = "hn-sidebar-loading";
        loadingDiv.textContent = "Loading...";
        sidebarContent.appendChild(loadingDiv);
        sidebar.scrollTop = 0;

        try {
            const html = await fetchItemHtml(itemId);

            if (lastSidebarItem !== itemId) return;

            // Store parsed top-level comments locally in memory
            currentThreadRoots = parseComments(html);

            saveThreadData(itemId, currentThreadRoots);

            sidebarContent.replaceChildren();

            if (!currentThreadRoots.length) {
                const emptyDiv = document.createElement("div");
                emptyDiv.className = "hn-sidebar-loading";
                emptyDiv.textContent = "No comments yet.";
                sidebarContent.appendChild(emptyDiv);
            } else {
                const fragment = document.createDocumentFragment();

                currentThreadRoots.forEach((comment) => {
                    fragment.appendChild(renderComment(comment));
                });

                sidebarContent.appendChild(fragment);
            }

            updateCounts();
        } catch {
            if (lastSidebarItem !== itemId) return;

            sidebarContent.replaceChildren();
            const errorDiv = document.createElement("div");
            errorDiv.className = "hn-sidebar-error";
            errorDiv.textContent = "Error loading comments.";
            sidebarContent.appendChild(errorDiv);
        }
    };

    // --- DOM Injection ---
    const addSidebarLinks = () => {
        const visitedItems = getVisitedItems();
        const truncatedIds = getTruncatedCommentIds();
        const threadData = getThreadData();

        document.querySelectorAll(".athing").forEach((row) => {
            const itemId = row.id;
            if (!itemId) return;

            const subtext = row.nextElementSibling?.querySelector(".subtext");
            if (!subtext || subtext.querySelector(".hn-sidebar-link")) return;

            const commentLink = [...subtext.querySelectorAll("a")].find((a) =>
                /comment|discuss/i.test(a.textContent)
            );

            if (!commentLink) return;

            const hasComments = /\d+\s+comment/i.test(commentLink.textContent);
            if (!hasComments) return;

            const triggerLink = document.createElement("a");
            triggerLink.href = "#";
            triggerLink.textContent = "--";
            triggerLink.className = "hn-sidebar-link";
            triggerLink.title = "Preview comments in sidebar";

            const fragment = document.createDocumentFragment();
            fragment.appendChild(document.createTextNode(" "));
            fragment.appendChild(triggerLink);

            if (visitedItems.includes(itemId)) {
                triggerLink.classList.add("active-closed");

                const itemData = threadData[itemId];
                if (itemData && Array.isArray(itemData.topCommentIds)) {
                    const truncatedCount = itemData.topCommentIds.filter((id) =>
                        truncatedIds.includes(String(id))
                    ).length;
                    const untruncatedCount = Math.max(0, itemData.totalTop - truncatedCount);

                    triggerLink.textContent = itemData.totalTop;

                    // Create dimmed truncate button on page load for previously fetched items
                    const truncateBtn = document.createElement("a");
                    truncateBtn.href = "#";
                    truncateBtn.textContent = untruncatedCount;
                    truncateBtn.className = "hn-sidebar-truncate-btn dimmed";
                    if (untruncatedCount === 0) {
                        truncateBtn.classList.add("zero-count");
                    }
                    truncateBtn.title = "Hide top visible comment (Spacebar)";

                    truncateBtn.addEventListener("click", (e) => {
                        e.preventDefault();
                        truncateTopComment();
                    });

                    fragment.appendChild(truncateBtn);
                }
            }

            triggerLink.addEventListener("click", (e) => {
                e.preventDefault();
                openSidebar(itemId, triggerLink);
            });

            subtext.appendChild(fragment);
        });
    };

    addSidebarLinks();
})();