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.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

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.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==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();
})();