X.com Quick Block Button

Adds a 'Quick Block' button to tweets on x.com for one-click blocking. Robust version.

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 or Violentmonkey 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         X.com Quick Block Button
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Adds a 'Quick Block' button to tweets on x.com for one-click blocking. Robust version.
// @author       Your Name Here
// @match        https://x.com/*
// @match        https://twitter.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const CHECK_INTERVAL = 1000;
    const CLICK_DELAY = 300;
    const BUTTON_TEXT = 'Block';
    const MAX_RETRIES = 3;

    // --- Styling for the Button ---
    const buttonStyle = `
        color: lightgray;
        background-color: transparent;
        border: none;
        margin-left: 10px;
        font-family: monospace;
        font-weight: bold;
        cursor: pointer;
        overflow: hidden; /* Clip the extra part of the shadow when scaled */
        position: relative; /* Needed for the pseudo-element */
    `;

      // --- Helper Function: Wait for an element to appear (RETRYING) ---
    async function waitForElement(selector, timeout = 2000, interval = 200, maxRetries = MAX_RETRIES) {
        return new Promise((resolve, reject) => {
            let elapsedTime = 0;
            let retries = 0;
            const timer = setInterval(() => {
                const element = document.querySelector(selector);
                if (element) {
                    clearInterval(timer);
                    resolve(element);
                } else {
                    elapsedTime += interval;
                    if (elapsedTime >= timeout) {
                        if (retries < maxRetries) {
                            retries++;
                            elapsedTime = 0;
                            console.warn(`Quick Block: Element not found, retrying (${retries}/${maxRetries}): ${selector}`);
                        } else {
                            clearInterval(timer);
                            reject(new Error(`Element not found after ${timeout}ms and ${maxRetries} retries: ${selector}`));
                        }
                    }
                }
            }, interval);
        });
    }

    // --- Helper Function: Simulate a click with delay ---
    async function clickElement(element) {
        if (element && typeof element.click === 'function') {
            try {
                element.click();
                await new Promise(resolve => setTimeout(resolve, CLICK_DELAY));
                return true;
            } catch (e) {
                console.error("Quick Block: Error clicking element:", e, element);
                return false;
            }
        } else {
            console.error("Quick Block: Invalid element or no click method:", element);
            return false;
        }
    }

    // --- Helper Function: Find Parent Tweet Article (Robust) ---
    function findParentTweetArticle(node) {
        let current = node;
        while (current && current !== document.body) {
            if (current.tagName === 'ARTICLE' && current.getAttribute('data-testid') === 'tweet') {
                return current;
            }
            current = current.parentNode;
        }
        return null;
    }

    // --- Helper Function: Find Action Bar (More Robust) ---
    function findActionBar(tweetArticle) {
       const selectors = [
            'div[role="group"]',
            'div:has(> button[data-testid="reply"])',
            'div:has(> button[data-testid="like"])',
            'div:has(> button[data-testid="retweet"])'
        ];

        for (const selector of selectors) {
            const found = tweetArticle.querySelector(selector);
            if (found) {
                return found;
            }
        }
        return null;
    }

    // --- Main Blocking Logic (Robust) ---
    async function quickBlockUser(event) {
        const button = event.target;
        button.disabled = true;
        button.textContent = 'Blocking...';
        button.style.backgroundImage = 'linear-gradient(to bottom right, #ff9800, #f57c00)'; // Orange gradient
        button.style.boxShadow = '0px 4px 10px rgba(0, 0, 0, 0.3)';

        const tweetArticle = findParentTweetArticle(button);
         if (!tweetArticle) {
            console.error('Quick Block: Could not find parent tweet article.', button, event.currentTarget);
            button.textContent = 'Error';
            button.style.backgroundColor = 'orange';
            return;
        }

        try {
            let moreButton = null;
            for (let i = 0; i < MAX_RETRIES; i++) {
                moreButton = tweetArticle.querySelector('button[data-testid="caret"]');
                if (moreButton) break;
                await new Promise(resolve => setTimeout(resolve, CLICK_DELAY * 2));
            }
            if (!moreButton) {
                throw new Error('Could not find More button (caret) after retries.');
            }

            if (!await clickElement(moreButton)) {
                throw new Error('Failed to click More button.');
            }

            let blockMenuItem = null;
             for (let i = 0; i < MAX_RETRIES; i++) {
                try{
                    blockMenuItem = await waitForElement('div[data-testid="block"]', 2000, 200, 1);
                    if(blockMenuItem){
                        break;
                    }
                }catch(e){
                    await new Promise(resolve => setTimeout(resolve, CLICK_DELAY * 2));
                }
             }
            if (!blockMenuItem) {
                throw new Error('Could not find Block menu item after retries.');
            }
            if (!await clickElement(blockMenuItem)) {
                throw new Error('Failed to click Block menu item.');
            }

            let confirmButton = null;
            for (let i = 0; i < MAX_RETRIES; i++) {
                try{
                    confirmButton = await waitForElement('button[data-testid="confirmationSheetConfirm"]', 2000, 200, 1);
                    if(confirmButton){
                        break;
                    }
                }catch(e){
                    await new Promise(resolve => setTimeout(resolve, CLICK_DELAY * 2));
                }
            }
            if (!confirmButton) {
                throw new Error('Could not find confirmation Block button after retries.');
            }
            if (!await clickElement(confirmButton)) {
                throw new Error('Failed to click confirmation Block button.');
            }

            console.log('Quick Block: User blocked successfully!');
            button.textContent = 'Blocked!';
            button.style.backgroundImage = 'linear-gradient(to bottom right, #4CAF50, #388E3C)'; // Green gradient
            button.style.boxShadow = '0px 4px 10px rgba(0, 0, 0, 0.3)';

        } catch (error) {
            console.error('Quick Block Error:', error.message, tweetArticle);
            button.textContent = 'Error';
            button.style.backgroundColor = 'orange';
            button.style.backgroundImage = 'none';
            setTimeout(() => {
                button.disabled = false;
                button.textContent = BUTTON_TEXT;
                button.style.backgroundImage =  'linear-gradient(to bottom right, #f44336, #d32f2f)';
                button.style.boxShadow = '0px 4px 10px rgba(0, 0, 0, 0.3)';
            }, 2000);
        }
    }

    // --- Function to Add Buttons to Tweets (Robust) ---
    function addBlockButtons() {
        const tweets = document.querySelectorAll('article[data-testid="tweet"]:not([data-quickblock-added])');

        tweets.forEach(tweet => {
            tweet.setAttribute('data-quickblock-added', 'true');

            const actionBar = findActionBar(tweet);
            if (actionBar) {
                const blockButton = document.createElement('button');
                blockButton.textContent = BUTTON_TEXT;
                blockButton.style.cssText = buttonStyle;
                blockButton.title = "Quickly block the user who posted this tweet";

                blockButton.addEventListener('mouseover', () => {
                    if (!blockButton.disabled) {
                        blockButton.style.cssText = buttonHoverStyle;
                    }
                });
                blockButton.addEventListener('mouseout', () => {
                    if (!blockButton.disabled && blockButton.textContent === BUTTON_TEXT) {
                        blockButton.style.cssText = buttonStyle;
                    }
                });
                blockButton.addEventListener('mousedown', () => {
                    if (!blockButton.disabled) {
                         blockButton.style.cssText = buttonActiveStyle;
                    }
                });
                blockButton.addEventListener('mouseup', () => {
                    if (!blockButton.disabled) {
                         blockButton.style.cssText = buttonStyle;
                    }
                });

                blockButton.addEventListener('click', quickBlockUser);

                if (!actionBar.querySelector('.quick-block-button')) {
                    blockButton.classList.add('quick-block-button');
                    actionBar.appendChild(blockButton);
                }

            } else {
                const tweetLinkElement = tweet.querySelector('a[href*="/status/"]');
                let tweetIdentifier = 'Tweet content unavailable or structure changed';
                if (tweetLinkElement && tweetLinkElement.href) {
                     tweetIdentifier = tweetLinkElement.href;
                 } else {
                     const userHandleElement = tweet.querySelector('a[href^="/"][role="link"] > div[dir="ltr"] > span');
                     if (userHandleElement && userHandleElement.textContent.startsWith('@')) {
                         tweetIdentifier = `Tweet by ${userHandleElement.textContent}`;
                     }
                 }
                console.warn(`Quick Block: Could not find action bar for tweet: ${tweetIdentifier}`, tweet);
            }
        });
    }

    // --- Run Periodically and Use MutationObserver ---
    addBlockButtons();

    const observer = new MutationObserver((mutationsList) => {
        let foundTweets = false;
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                 mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE && (node.matches('article[data-testid="tweet"]') || node.querySelector('article[data-testid="tweet"]'))) {
                       foundTweets = true;
                    }
                 });
            }
            if (foundTweets) break;
        }

        if (foundTweets) {
            requestAnimationFrame(addBlockButtons);
        }
    });

    const mainContentArea = document.querySelector('main');
    if (mainContentArea) {
        observer.observe(mainContentArea, { childList: true, subtree: true });
    } else {
        console.warn("Quick Block: Could not find <main> element, observing document.body. This might be less efficient.");
        observer.observe(document.body, { childList: true, subtree: true });
    }

    setInterval(addBlockButtons, CHECK_INTERVAL * 2);

    console.log('X.com Quick Block script loaded (v1.3 - Stylish).');

})();