BigNaturals / Simple 1-Click Block for X (Twitter)

// ==UserScript==
// @name               Simple 1-Click Block for X (Twitter)
// @name:pt-BR         Bloqueio com 1 Clique para X (Twitter)
// @name:es            Bloqueo con 1 Clic para X (Twitter)
// @name:zh-CN         适用于 X(Twitter)的一键拉黑脚本
// @name:ja            X (Twitter) 1クリックブロック
// @name:id            Blokir 1-Klik Simpel para X (Twitter)
// @name:pl            Proste blokowanie jednym kliknięciem dla X (Twitter)
// @name:hi            X (Twitter) के लिए सरल 1-क्लिक ब्लॉक
// @name:tr            X (Twitter) için Kolay Tek Tıkla Engelleme
// @name:de            Einfache 1-Klick-Sperre für X (Twitter)
// @name:ar            حظر بسيط بنقرة واحدة لـ X (Twitter)
// @name:th            บล็อกเพียงคลิกเดียวสำหรับ X (Twitter)
// @name:fr            Blocage simple en un clic pour X (Twitter)
// @name:ko            X (Twitter) 간편 클릭 차단 스크립트
// @name:zh-HK         適用於 X (Twitter) 的一鍵封鎖指令碼
// @namespace          Violentmonkey Scripts
// @version            1.1
// @description        Adds a 1-click block button to tweets and user profiles on X (Twitter).
// @description:pt-BR  Adiciona um botão de bloqueio com 1 clique aos tweets e perfis de usuários no X (Twitter).
// @description:es     Añade un botón de bloqueo con 1 clic a los tweets y perfiles de usuario en X (Twitter).
// @description:zh-CN  在 X(Twitter)的推文和用户主页中添加一键拉黑按钮。
// @description:ja     X (Twitter) のツイートとユーザープロファイルに1クリックブロックボタンを追加します。
// @description:id     Menambahkan tombol blokir 1-klik ke tweet dan profil pengguna di X (Twitter).
// @description:pl     Dodaje przycisk blokowania jednym kliknięciem do tweetów i profili użytkowników w serwisie X (Twitter).
// @description:hi     X (Twitter) पर ट्वीट्स और उपयोगकर्ता प्रोफाइल में 1-क्लिक ब्लॉक बटन जोड़ता है।
// @description:tr     X (Twitter) üzerindeki tweetlere ve kullanıcı profillerine tek tıkla engelleme butonu ekler.
// @description:de     Fügt Tweets und Benutzerprofilen auf X (Twitter) eine 1-Klick-Sperrschaltfläche hinzu.
// @description:ar     يضيف زر حظر بنقرة واحدة إلى التغريدات وملفات تعريف المستخدمين على X (Twitter).
// @description:th     เพิ่มปุ่มบล็อกเพียงคลิกเดียวในการทวีตและโปรไฟล์ผู้ใช้บน X (Twitter)
// @description:fr     Ajoute un bouton de blocage en un clic aux tweets et aux profils d'utilisateurs sur X (Twitter).
// @description:ko     X (Twitter) 트윗과 사용자 프로필에 간편 클릭 차단 버튼을 추가합니다.
// @description:zh-HK  在 X (Twitter) 的推文和用戶個人檔案中添加一鍵封鎖按鈕。
// @license            MIT
// @author             Big Naturals
// @homepage           https://greasyfork.org/en/users/1568161-big-naturals
// @icon               https://i.postimg.cc/MKbdzqBy/onelickblock.png
// @contributionURL    https://www.blockchain.com/explorer/addresses/btc/16JXciLoAs6R8iQjmpKqWLSNreC1epfz6R
// @compatible chrome
// @compatible firefox
// @compatible opera
// @compatible safari
// @compatible edge
// @match              *://x.com/*
// @match              *://twitter.com/*
// @grant              none
// @run-at             document-idle
// ==/UserScript==

(function() {
    'use strict';

    const blockMenuPathSnippet = 'M12 3.75c-4.55';

    const blockSvg = `
        <svg viewBox="0 0 24 24" aria-hidden="true" class="quick-block-svg">
            <g><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8 0-1.87.64-3.6 1.72-5.01l11.29 11.29C15.6 19.36 13.87 20 12 20zm6.28-2.99L6.99 5.72C8.4 4.64 10.13 4 12 4c4.41 0 8 3.59 8 8 0 1.87-.64 3.6-1.72 5.01z"></path></g>
        </svg>
    `;

    const btnStyle = `
        background-color: transparent;
        border: none;
        border-radius: 9999px;
        width: 30px;
        height: 30px;
        margin-right: 2px;
        cursor: pointer;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        transition: background-color 0.2s;
        z-index: 10;
    `;

    const styleSheet = document.createElement("style");
    styleSheet.textContent = `
        .quick-block-svg {
            width: 18px;
            height: 18px;
            fill: #71767b;
            transition: fill 0.2s ease-in-out;
        }
        .quick-block-btn-feed:hover .quick-block-svg,
        .quick-block-btn-profile:hover .quick-block-svg {
            fill: rgb(244, 33, 46);
        }
        .quick-block-btn-feed:hover,
        .quick-block-btn-profile:hover {
            background-color: rgba(244, 33, 46, 0.1) !important;
        }
        /* Ensure profile button aligns visually with action buttons */
        .quick-block-btn-profile {
            margin-left: 8px;
            margin-right: 8px;
        }
    `;
    document.head.appendChild(styleSheet);

    const randomDelay = (min, max) =>
        new Promise(res => setTimeout(res, Math.floor(Math.random() * (max - min + 1) + min)));

    function getLoggedUsername() {
        const accountBtn = document.querySelector('[data-testid="SideNav_AccountSwitcher_Button"]');
        if (!accountBtn) return null;

        const usernameSpan = accountBtn.querySelector('span');
        const allSpans = accountBtn.querySelectorAll('span');
        
        for (let span of allSpans) {
            const text = span.textContent.trim();
            if (text.startsWith('@')) {
                return text.substring(1);
            }
        }

        const avatarContainer = accountBtn.querySelector('[data-testid^="UserAvatar-Container-"]');
        if (avatarContainer) {
            return avatarContainer.getAttribute('data-testid').replace('UserAvatar-Container-', '');
        }

        return null;
    }

    function getProfileUsernameFromPath() {
        const m = window.location.pathname.match(/^\/([^\/?#]+)/);
        return m ? m[1] : null;
    }

    function isOwnTweet(tweet, loggedUsername) {
        if (!loggedUsername) return false;

        const analyticsLink = tweet.querySelector('a[href$="/analytics"]');
        if (!analyticsLink) return false;

        const href = analyticsLink.getAttribute('href');
        const regex = new RegExp(`^\\/${loggedUsername}\\/status\\/\\d+\\/analytics$`);
        return regex.test(href);
    }

    async function performBlock(triggerElement) {
        if (!triggerElement) return;

        triggerElement.click();
        await randomDelay(50, 100);

        const menuItems = document.querySelectorAll('[role="menuitem"], [role="menuitemradio"]');
        const blockOption = Array.from(menuItems).find(item => {
            const pathEl = item.querySelector('svg path');
            const d = pathEl && pathEl.getAttribute && pathEl.getAttribute('d');
            return d && d.includes(blockMenuPathSnippet);
        });

        if (blockOption) {
            blockOption.click();
            await randomDelay(50, 100);
            const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
            if (confirmBtn) confirmBtn.click();
        } else {
            triggerElement.click();
        }
    }

    function createBlockBtnForCaret(caret) {
        const btn = document.createElement('button');
        btn.innerHTML = blockSvg;
        btn.setAttribute('style', btnStyle);
        btn.className = 'quick-block-btn-feed';
        btn.title = 'Block user';
        btn.onclick = (e) => {
            e.preventDefault();
            e.stopPropagation();
            performBlock(caret);
        };
        return btn;
    }

    function createBlockBtnForProfile(moreButton) {
        const btn = moreButton.cloneNode(true);

        btn.removeAttribute('data-testid');
        btn.removeAttribute('aria-expanded');
        btn.removeAttribute('aria-haspopup');

        btn.classList.add('quick-block-btn-profile');

        btn.setAttribute('style', moreButton.getAttribute('style') || '');

        const svgContainer = btn.querySelector('svg');
        if (svgContainer) {
            svgContainer.outerHTML = `
                <svg viewBox="0 0 24 24" aria-hidden="true" class="${svgContainer.className.baseVal}">
                    <g>
                        <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8 0-1.87.64-3.6 1.72-5.01l11.29 11.29C15.6 19.36 13.87 20 12 20zm6.28-2.99L6.99 5.72C8.4 4.64 10.13 4 12 4c4.41 0 8 3.59 8 8 0 1.87-.64 3.6-1.72 5.01z"></path>
                    </g>
                </svg>
            `;
        }

        btn.onclick = (e) => {
            e.preventDefault();
            e.stopPropagation();
            performBlock(moreButton);
        };

        return btn;
    }

    function injectFeedButtons(loggedUsername) {
        if (!loggedUsername) return;

        const tweets = document.querySelectorAll('[data-testid="tweet"]');

        tweets.forEach(tweet => {
            if (tweet.querySelector('.quick-block-btn-feed')) return;
            if (isOwnTweet(tweet, loggedUsername)) return;

            const caret = tweet.querySelector('[data-testid="caret"]');
            if (caret && caret.parentNode) {
                const blockBtn = createBlockBtnForCaret(caret);
                caret.parentNode.insertBefore(blockBtn, caret);
            }
        });
    }

    function injectProfileButton(loggedUsername) {
        const profileUsername = getProfileUsernameFromPath();
        if (!profileUsername) return;
        if (!loggedUsername) return;
        if (profileUsername === loggedUsername) return;

        const moreButton = document.querySelector('[data-testid="userActions"]');
        if (!moreButton) return;

        const parent = moreButton.parentNode;
        if (!parent) return;
        if (parent.querySelector('.quick-block-btn-profile')) return;

        const blockBtn = createBlockBtnForProfile(moreButton);
        parent.insertBefore(blockBtn, moreButton);
    }

    function injectAll() {
        const loggedUsername = getLoggedUsername();
        if (!loggedUsername) return;

        injectFeedButtons(loggedUsername);
        injectProfileButton(loggedUsername);
    }

    let timeout = null;
    const observer = new MutationObserver(() => {
        clearTimeout(timeout);
        timeout = setTimeout(injectAll, 150);
    });

    observer.observe(document.body, { childList: true, subtree: true });

    injectAll();
})();