GitHub Discussion Load-More & Expand Collapsed Auto-Loader

Automatically clicks "Load more" on GitHub discussion pages until all comments are loaded AND expands all collapsed comments

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         GitHub Discussion Load-More & Expand Collapsed Auto-Loader
// @namespace    http://tampermonkey.net/
// @version      1.3.1
// @description  Automatically clicks "Load more" on GitHub discussion pages until all comments are loaded AND expands all collapsed comments
// @author       Premysl Karbula (https://github.com/smuuf)
// @license      MIT
// @match        https://github.com/orgs/*/discussions/*
// @match        https://github.com/*/*/discussions/*
// @match        https://github.com/*/*/issues/*
// @match        https://github.com/*/*/pulls/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    let loadMoreRunning = false;
    let expandRunning = false;
    let loadIntervalId = null;
    let expandIntervalId = null;
    let clickCount = 0;
    let expandCount = 0;
    
    const CLICK_DELAY_MS = 1200;
    const PANEL_ID = 'gh-autoloader-panel-' + Math.random().toString(36).substr(2, 6);

    // --- Find the "Load more" button ---
    function findLoadMoreButton() {
        // Primary: ajax-pagination-btn class (matches the HTML you provided)
        const ajaxBtn = document.querySelector('.ajax-pagination-btn');
        if (ajaxBtn && ajaxBtn.offsetParent !== null) return ajaxBtn;

        // Fallback: any button/link containing "Load more" text
        const allButtons = document.querySelectorAll(`button:not(#${PANEL_ID} *), a:not(#${PANEL_ID} *)`);
        for (const el of allButtons) {
            if (/load more/i.test(el.textContent) && el.offsetParent !== null) {
                return el;
            }
        }
        return null;
    }

    // --- Find and click all collapsed comment buttons ---
    function expandCollapsedComments() {
        let expanded = 0;
        
        // Method 1: Look for buttons with octicon-unfold icon
        const unfoldButtons = document.querySelectorAll('button svg.octicon-unfold');
        unfoldButtons.forEach(svg => {
            const button = svg.closest('button');
            if (button && button.offsetParent !== null && !button.disabled) {
                button.click();
                expanded++;
            }
        });

        // Method 2: Look for buttons with tooltip text "show comment"
        const tooltips = document.querySelectorAll('[id*="_r_"]');
        tooltips.forEach(tooltip => {
            if (tooltip.textContent.toLowerCase().includes('show comment')) {
                const buttonId = tooltip.getAttribute('aria-labelledby') || tooltip.id;
                if (buttonId) {
                    const button = document.querySelector(`[aria-labelledby="${buttonId}"]`);
                    if (button && button.offsetParent !== null && !button.disabled) {
                        button.click();
                        expanded++;
                    }
                }
            }
        });

        // Method 3: Look for buttons in CommentActions with unfold icon
        const commentActionButtons = document.querySelectorAll('.CommentActions-module__CommentActionsIconButton__Fsg4J');
        commentActionButtons.forEach(button => {
            const unfoldIcon = button.querySelector('.octicon-unfold');
            if (unfoldIcon && button.offsetParent !== null && !button.disabled) {
                button.click();
                expanded++;
            }
        });

        return expanded;
    }

    // --- Status text helpers ---
    function statusText() {
        const details = [];
        
        if (loadMoreRunning) {
            details.push(`Loading… (${clickCount})`);
        } else if (clickCount > 0) {
            details.push(`${clickCount} loads`);
        }
        
        if (expandRunning) {
            details.push(`Expanding… (${expandCount})`);
        } else if (expandCount > 0) {
            details.push(`${expandCount} expanded`);
        }
        
        return details.length > 0 ? details.join(' | ') : 'Idle';
    }

    // --- Core loop for Load More ---
    function tickLoadMore() {
        const btn = findLoadMoreButton();
        if (btn) {
            btn.click();
            clickCount++;
            updateUI();
        } else {
            // No more "Load more" buttons
            stopLoadMore(true);
        }
    }

    // --- Core loop for Expand Collapsed ---
    function tickExpand() {
        const expanded = expandCollapsedComments();
        if (expanded > 0) {
            expandCount += expanded;
            updateUI();
        } else {
            // No more collapsed comments
            stopExpand(true);
        }
    }

    function startLoadMore() {
        if (loadMoreRunning) return;
        loadMoreRunning = true;
        clickCount = 0;
        updateUI();
        tickLoadMore();
        loadIntervalId = setInterval(tickLoadMore, CLICK_DELAY_MS);
    }

    function stopLoadMore(auto = false) {
        if (!loadMoreRunning && !auto) return;
        loadMoreRunning = false;
        clearInterval(loadIntervalId);
        loadIntervalId = null;
        updateUI();
    }

    function startExpand() {
        if (expandRunning) return;
        expandRunning = true;
        expandCount = 0;
        updateUI();
        tickExpand();
        expandIntervalId = setInterval(tickExpand, CLICK_DELAY_MS);
    }

    function stopExpand(auto = false) {
        if (!expandRunning && !auto) return;
        expandRunning = false;
        clearInterval(expandIntervalId);
        expandIntervalId = null;
        updateUI();
    }

    function startBoth() {
        startLoadMore();
        startExpand();
    }

    function stopBoth() {
        stopLoadMore();
        stopExpand();
    }

    // --- Build floating UI ---
    const panel = document.createElement('div');
    panel.id = PANEL_ID
    panel.style.cssText = `
        position: fixed;
        bottom: 24px;
        right: 24px;
        z-index: 99999;
        background: #161b22;
        border: 1px solid #30363d;
        border-radius: 10px;
        padding: 12px 16px;
        display: flex;
        flex-direction: column;
        gap: 8px;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
        font-size: 13px;
        color: #e6edf3;
        box-shadow: 0 8px 24px rgba(0,0,0,0.4);
        min-width: 200px;
        user-select: none;
    `;

    const title = document.createElement('div');
    title.textContent = '⚡ Discussion Auto-Loader';
    title.style.cssText = 'font-weight: 600; font-size: 13px; color: #58a6ff;';

    const label = document.createElement('div');
    label.style.cssText = 'color: #8b949e; font-size: 12px;';
    label.textContent = statusText();

    const btnRow = document.createElement('div');
    btnRow.style.cssText = 'display: flex; gap: 8px;';

    // Add hover styles
    const styleSheet = document.createElement('style');
    styleSheet.textContent = `
        .gh-autoloader-btn {
            flex: 1;
            padding: 8px 12px;
            border-radius: 6px;
            font-size: 12px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s ease;
            box-shadow: 0 1px 3px rgba(0,0,0,0.12);
            white-space: nowrap;
        }
        .gh-autoloader-btn:hover:not(:disabled) {
            transform: translateY(-1px);
            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
        }
        .gh-autoloader-btn:active:not(:disabled) {
            transform: translateY(0);
            box-shadow: 0 1px 3px rgba(0,0,0,0.12);
        }
        .gh-autoloader-btn:disabled {
            cursor: not-allowed;
            opacity: 0.5;
        }
        .gh-autoloader-btn-load {
            border: 1px solid #238636;
            background: linear-gradient(180deg, #2ea043 0%, #238636 100%);
            color: #fff;
        }
        .gh-autoloader-btn-load:hover:not(:disabled) {
            background: linear-gradient(180deg, #3fb950 0%, #2ea043 100%);
            border-color: #2ea043;
        }
        .gh-autoloader-btn-expand {
            border: 1px solid #1f6feb;
            background: linear-gradient(180deg, #388bfd 0%, #1f6feb 100%);
            color: #fff;
        }
        .gh-autoloader-btn-expand:hover:not(:disabled) {
            background: linear-gradient(180deg, #539bf5 0%, #388bfd 100%);
            border-color: #388bfd;
        }
        .gh-autoloader-btn-both {
            border: 1px solid #a371f7;
            background: linear-gradient(180deg, #b083f0 0%, #a371f7 100%);
            color: #fff;
        }
        .gh-autoloader-btn-both:hover:not(:disabled) {
            background: linear-gradient(180deg, #bc8cff 0%, #b083f0 100%);
            border-color: #b083f0;
        }
        .gh-autoloader-btn-stop {
            border: 1px solid #30363d;
            background: linear-gradient(180deg, #21262d 0%, #161b22 100%);
            color: #8b949e;
        }
        .gh-autoloader-btn-stop:hover:not(:disabled) {
            background: linear-gradient(180deg, #30363d 0%, #21262d 100%);
            border-color: #484f58;
            color: #c9d1d9;
        }
        .gh-autoloader-btn-stop.active {
            border-color: #da3633;
            background: linear-gradient(180deg, #b62324 0%, #8e1b1d 100%);
            color: #ffdcd7;
        }
        .gh-autoloader-btn-stop.active:hover:not(:disabled) {
            background: linear-gradient(180deg, #da3633 0%, #b62324 100%);
            border-color: #f85149;
        }
    `;
    document.head.appendChild(styleSheet);

    const loadMoreBtn = document.createElement('button');
    loadMoreBtn.textContent = '▶ Load More';
    loadMoreBtn.className = 'gh-autoloader-btn gh-autoloader-btn-load';

    const expandBtn = document.createElement('button');
    expandBtn.textContent = '↕ Expand';
    expandBtn.className = 'gh-autoloader-btn gh-autoloader-btn-expand';

    const btnRow2 = document.createElement('div');
    btnRow2.style.cssText = 'display: flex; gap: 8px;';

    const startBothBtn = document.createElement('button');
    startBothBtn.textContent = '▶ Both';
    startBothBtn.className = 'gh-autoloader-btn gh-autoloader-btn-both';

    const stopBtn = document.createElement('button');
    stopBtn.textContent = '⏹ Stop All';
    stopBtn.className = 'gh-autoloader-btn gh-autoloader-btn-stop';

    const delayRow = document.createElement('div');
    delayRow.style.cssText = 'display: flex; align-items: center; gap: 8px; color: #8b949e;';

    const delayLabel = document.createElement('label');
    delayLabel.textContent = 'Delay (ms):';
    delayLabel.style.fontSize = '12px';

    const delayInput = document.createElement('input');
    delayInput.type = 'number';
    delayInput.value = CLICK_DELAY_MS;
    delayInput.min = 300;
    delayInput.max = 10000;
    delayInput.step = 100;
    delayInput.style.cssText = `
        width: 70px;
        background: #0d1117;
        border: 1px solid #30363d;
        border-radius: 6px;
        color: #e6edf3;
        padding: 3px 6px;
        font-size: 12px;
    `;

    // Drag support
    let dragging = false, ox = 0, oy = 0;
    title.style.cursor = 'grab';
    title.addEventListener('mousedown', e => {
        dragging = true;
        const r = panel.getBoundingClientRect();
        ox = e.clientX - r.left;
        oy = e.clientY - r.top;
        title.style.cursor = 'grabbing';
    });
    document.addEventListener('mousemove', e => {
        if (!dragging) return;
        panel.style.left = (e.clientX - ox) + 'px';
        panel.style.top = (e.clientY - oy) + 'px';
        panel.style.right = 'auto';
        panel.style.bottom = 'auto';
    });
    document.addEventListener('mouseup', () => {
        dragging = false;
        title.style.cursor = 'grab';
    });

    function updateUI() {
        label.textContent = statusText();
        
        loadMoreBtn.disabled = loadMoreRunning;
        expandBtn.disabled = expandRunning;
        startBothBtn.disabled = loadMoreRunning || expandRunning;
        
        const anyRunning = loadMoreRunning || expandRunning;
        if (anyRunning) {
            stopBtn.classList.add('active');
        } else {
            stopBtn.classList.remove('active');
        }
    }

    loadMoreBtn.addEventListener('click', () => {
        clickCount = 0;
        startLoadMore();
    });

    expandBtn.addEventListener('click', () => {
        expandCount = 0;
        startExpand();
    });

    startBothBtn.addEventListener('click', () => {
        clickCount = 0;
        expandCount = 0;
        startBoth();
    });

    stopBtn.addEventListener('click', () => stopBoth());

    delayRow.append(delayLabel, delayInput);
    btnRow.append(loadMoreBtn, expandBtn);
    btnRow2.append(startBothBtn, stopBtn);
    panel.append(title, label, btnRow, btnRow2, delayRow);
    document.body.appendChild(panel);

    updateUI();

})();