Twitter/X Archive - bypass infinite scroll showing only part of a page at a time (Robust Sync)

Archive tweets into a sidebar and keep them continuously synced, even after multiple "Show more" clicks or React re-renders.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Twitter/X Archive - bypass infinite scroll showing only part of a page at a time (Robust Sync)
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Archive tweets into a sidebar and keep them continuously synced, even after multiple "Show more" clicks or React re-renders.
// @author       You
// @match        https://twitter.com/*
// @match        https://x.com/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // --- Create archive sidebar ---
    const archive = document.createElement('div');
    Object.assign(archive.style, {
        position: 'fixed',
        top: '50px',
        right: '0',
        width: '450px',
        height: '90%',
        overflowY: 'auto',
        background: 'white',
        borderLeft: '2px solid #ccc',
        zIndex: 99999,
        fontSize: '14px',
        padding: '8px',
        boxSizing: 'border-box'
    });
    archive.innerHTML = "<h3 style='margin:4px 0 8px;font-weight:bold;'>Archived Tweets (Robust Sync)</h3><div id='archive-contents'></div>";
    document.body.appendChild(archive);
    const archiveContents = archive.querySelector('#archive-contents');

    // --- Track tweets ---
    const seen = new Map(); // tweetId → { original, clone, observer }

    // --- Helper: create clone container ---
    function makeCloneContainer(article) {
        const div = document.createElement("div");
        div.style.border = "1px solid #ddd";
        div.style.margin = "6px 0";
        div.style.padding = "6px";
        div.style.background = "#fff";
        div.style.maxWidth = "100%";
        div.innerHTML = article.innerHTML;
        return div;
    }

    // --- Attach a new observer to keep clone synced ---
    function attachObserver(tweetId, article, clone) {
        // Disconnect old one if exists
        if (seen.get(tweetId)?.observer) {
            seen.get(tweetId).observer.disconnect();
        }

        const innerObserver = new MutationObserver(() => {
            clone.innerHTML = article.innerHTML;
        });

        innerObserver.observe(article, { childList: true, subtree: true, characterData: true });

        // Update mapping
        seen.set(tweetId, { original: article, clone, observer: innerObserver });
    }

    // --- Process a tweet article ---
    function processArticle(article) {
        const link = article.querySelector('a[href*="/status/"]');
        if (!link) return;
        const tweetId = link.getAttribute('href');
        if (!tweetId) return;

        // Already archived?
        if (seen.has(tweetId)) {
            const entry = seen.get(tweetId);
            if (entry.original !== article) {
                // Original was replaced → reattach observer
                console.log("♻️ Tweet DOM replaced, reattaching observer:", tweetId);
                attachObserver(tweetId, article, entry.clone);
            }
            return;
        }

        // First time seeing this tweet
        const clone = makeCloneContainer(article);
        archiveContents.appendChild(clone);
        console.log("📌 Archived new tweet:", tweetId);

        attachObserver(tweetId, article, clone);
    }

    // --- Global observer for new tweets ---
    const observer = new MutationObserver(mutations => {
        for (let m of mutations) {
            for (let node of m.addedNodes) {
                if (node.nodeType !== 1) continue;

                if (node.tagName === "ARTICLE") {
                    processArticle(node);
                }

                const articles = node.querySelectorAll ? node.querySelectorAll("article") : [];
                articles.forEach(a => processArticle(a));
            }
        }
    });

    // --- Attach to timeline ---
    function watchTimeline() {
        const timeline = document.querySelector('[role="feed"], [aria-label^="Timeline"]');
        if (timeline) {
            console.log("✅ Archive: timeline found, robust syncing active...");
            const articles = timeline.querySelectorAll("article");
            articles.forEach(a => processArticle(a));
            observer.observe(timeline, { childList: true, subtree: true });
        } else {
            console.log("⏳ Archive: timeline not found yet, retrying...");
            setTimeout(watchTimeline, 1000);
        }
    }

    watchTimeline();
})();