Torn Chat Blocker

Aggressively blocks Torn Chat with a visual toggle switch and per-page always-block via long press.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Torn Chat Blocker
// @namespace    https://greasyfork.org/en/users/1431907-theeeunknown
// @version      1.2
// @description  Aggressively blocks Torn Chat with a visual toggle switch and per-page always-block via long press.
// @author       TR0LL
// @license      MIT
// @match        https://www.torn.com/*
// @grant        GM_addStyle
// @grant        unsafeWindow
// @run-at       document-start
// ==/UserScript==
 
(function() {
    'use strict';
 
    // --- Configuration ---
    const DEBUG_MODE = false; // Set to true for detailed console logging, false for production
    const CHAT_URL_PATTERNS_TO_BLOCK = [
        // General chat patterns
        /https:\/\/www\.torn\.com\/chat/,
        /https:\/\/www\.torn\.com\/builds\/chat\//,
        /chat-worker\.js/,
        /fetch-worker\.js/,
        /https:\/\/www\.torn\.com\/js\/chat/,
 
        // Specific script files
        /https:\/\/www\.torn\.com\/builds\/chat\/app\.[a-f0-9]+\.js/,
        /https:\/\/www\.torn\.com\/builds\/chat\/vendors\.[a-f0-9]+\.js/,
        /https:\/\/www\.torn\.com\/builds\/chat\/runtime\.[a-f0-9]+\.js/,
 
        // Specific CSS files
        /https:\/\/www\.torn\.com\/builds\/chat\/app\.[a-f0-9]+\.css/,
        /https:\/\/www\.torn\.com\/builds\/chat\/vendors\.[a-f0-9]+\.css/,
 
        // Ultra-aggressive chat asset blocking
        /torn\.com\/builds\/chat\/.*\.js/,
        /torn\.com\/builds\/chat\/.*\.css/,
        /torn\.com\/builds\/chat\/.*\.json/,
        /torn\.com\/builds\/chat\/.*\.png/,
        /torn\.com\/builds\/chat\/.*\.jpg/,
        /torn\.com\/builds\/chat\/.*\.svg/,
        /torn\.com\/builds\/chat\/.*\.woff/,
        /torn\.com\/builds\/chat\/.*\.mp3/,
 
        // Sendbird related patterns
        /sendbird\.(com|io)/,
        /api\.sendbird\.com/,
        /ws\.sendbird\.com/,
        /sb\.scorpion\.io/,
        /^\wss?:\/\/.*sendbird/,
        /.*\.sendbird\..*/i,
 
        // Broader chat patterns
        /torn\.com\/.*chat/i,
        /torn\.com\/.*sendbird/i,
 
        // Added rule for profile-mini (Example, adjust as needed)
        /https:\/\/www\.torn\.com\/builds\/profile-mini\//
    ];
 
    const TOGGLE_BUTTON_ID = 'torn-chat-blocker-toggle';
    const LOCAL_STORAGE_KEY_GLOBAL_BLOCK = 'tornChatBlockingEnabled';
    const LOCAL_STORAGE_KEY_ALWAYS_BLOCK_PAGES = 'tornChatAlwaysBlockedPages';
    const DEBOUNCE_DELAY = 250;
    const LONG_PRESS_DURATION = 750; // ms
 
    // --- Global State ---
    let isBlockingEnabled = localStorage.getItem(LOCAL_STORAGE_KEY_GLOBAL_BLOCK) !== 'false'; // Defaults to true
    let alwaysBlockedPages = new Set(JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY_ALWAYS_BLOCK_PAGES) || '[]'));
    let domObserver = null;
    let longPressTimer = null;
    let isLongPressFlag = false; // Distinguish long press from click
 
    // --- Logging Utility ---
    function log(...args) {
        if (DEBUG_MODE) {
            console.log('Torn Chat Blocker:', ...args);
        }
    }
    function warn(...args) {
        if (DEBUG_MODE) {
            console.warn('Torn Chat Blocker:', ...args);
        }
    }
 
    // --- CSS for the toggle button (Switch Style) ---
    GM_addStyle(`
        #${TOGGLE_BUTTON_ID} {
            position: fixed;
            top: 10px;
            right: 10px;
            z-index: 9999;
            width: 50px;
            height: 26px;
            border-radius: 13px;
            cursor: pointer;
            transition: background-color 0.3s ease, border-color 0.3s ease;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1), inset 0 1px 1px rgba(0,0,0,0.1);
            box-sizing: border-box;
            -webkit-appearance: none;
            -moz-appearance: none;
            appearance: none;
            padding: 0;
            font-size: 0;
            line-height: 0;
            color: transparent;
            /* Default background/border will be set by specific classes */
        }
 
        #${TOGGLE_BUTTON_ID}::after { /* The slider knob */
            content: "";
            position: absolute;
            top: 2px;
            left: 2px;
            width: 20px;
            height: 20px;
            background-color: white;
            border-radius: 50%;
            box-shadow: 0 1px 3px rgba(0,0,0,0.4);
            transition: transform 0.3s ease;
            box-sizing: border-box;
        }
 
        /* State when global blocking is ON (GREEN) */
        #${TOGGLE_BUTTON_ID}.blocking-on {
            background-color: #388e3c; /* Green */
            border: 1px solid #2e7d32; /* Darker green */
        }
        #${TOGGLE_BUTTON_ID}.blocking-on::after {
            transform: translateX(24px); /* Knob right */
        }
 
        /* State when global blocking is OFF (RED) */
        #${TOGGLE_BUTTON_ID}.blocking-off {
            background-color: #d32f2f; /* Red */
            border: 1px solid #c62828; /* Darker red */
        }
        #${TOGGLE_BUTTON_ID}.blocking-off::after {
            transform: translateX(0); /* Knob left */
        }
 
        /* State when current page is ALWAYS BLOCKED (ORANGE) */
        #${TOGGLE_BUTTON_ID}.blocking-always {
            background-color: #FFA500; /* Orange */
            border: 1px solid #E69500; /* Darker orange */
        }
        #${TOGGLE_BUTTON_ID}.blocking-always::after {
            transform: translateX(24px); /* Knob right (appears "ON") */
        }
 
        #${TOGGLE_BUTTON_ID}:hover {
             filter: brightness(1.1);
        }
    `);
 
    // --- Core Blocking Logic ---
    function isCurrentPageAlwaysBlocked() {
        return alwaysBlockedPages.has(window.location.href);
    }
 
    // Determines if chat/sendbird assets should be blocked for the given requestUrl
    function shouldBlockUrl(requestUrl) {
        const pageIsAlwaysBlocked = isCurrentPageAlwaysBlocked();
        const effectiveBlockingActive = isBlockingEnabled || pageIsAlwaysBlocked;
 
        if (!effectiveBlockingActive) {
            return false; // Neither global nor page-specific override says block
        }
 
        // If blocking is active (globally or for this page), check patterns
        if (requestUrl && typeof requestUrl === 'string') {
            for (const pattern of CHAT_URL_PATTERNS_TO_BLOCK) {
                if (pattern.test(requestUrl)) {
                    log(`Blocking request to ${requestUrl} by pattern ${pattern} (Global: ${isBlockingEnabled}, PageAlwaysBlocked: ${pageIsAlwaysBlocked})`);
                    return true;
                }
            }
            // Fallback keyword check
            if (requestUrl.includes('chat') || requestUrl.includes('sendbird')) {
                if (!CHAT_URL_PATTERNS_TO_BLOCK.some(p => p.test(requestUrl))) { // Log only if not caught by a specific pattern
                    log(`Blocking request to ${requestUrl} by keyword fallback (Global: ${isBlockingEnabled}, PageAlwaysBlocked: ${pageIsAlwaysBlocked})`);
                }
                return true;
            }
        }
        return false;
    }
 
    // Determines if DOM elements related to chat should be hidden on the current page
    function pageShouldHaveElementsHidden() {
        return isBlockingEnabled || isCurrentPageAlwaysBlocked();
    }
 
    // --- Request Interception ---
    const originalFetch = unsafeWindow.fetch;
    const originalXHRopen = unsafeWindow.XMLHttpRequest.prototype.open;
    const originalWebSocket = unsafeWindow.WebSocket;
 
    unsafeWindow.fetch = function(...args) {
        const requestInfo = args[0];
        const url = (typeof requestInfo === 'string') ? requestInfo : requestInfo.url;
        if (shouldBlockUrl(url)) {
            log(`Blocking fetch request to ${url}`);
            return Promise.reject(new Error(`Torn Chat Blocker: Request to ${url} blocked`));
        }
        return originalFetch.apply(unsafeWindow, args);
    };
 
    unsafeWindow.XMLHttpRequest.prototype.open = function(...args) {
        const method = args[0];
        const url = args[1];
        if (shouldBlockUrl(url)) {
            log(`Preparing to block XHR ${method} request to ${url}`);
            this._blockedUrl = url;
            this._isBlockedByScript = true;
        } else {
            this._isBlockedByScript = false;
        }
        return originalXHRopen.apply(this, args);
    };
 
    const originalXHRSend = unsafeWindow.XMLHttpRequest.prototype.send;
    unsafeWindow.XMLHttpRequest.prototype.send = function(...args) {
        if (this._isBlockedByScript && this._blockedUrl) {
            log(`Preventing XHR send for ${this._blockedUrl}`);
            const xhrInstance = this;
            setTimeout(() => {
                const errorEvent = new ProgressEvent('error');
                if (typeof xhrInstance.onerror === 'function') xhrInstance.onerror(errorEvent);
                if (typeof xhrInstance.onloadend === 'function') xhrInstance.onloadend(errorEvent);
                try {
                    xhrInstance.dispatchEvent(new Event('error'));
                    xhrInstance.dispatchEvent(new Event('loadend'));
                } catch (e) {
                    warn('Error dispatching events on blocked XHR:', e);
                }
            }, 10);
            return;
        }
        return originalXHRSend.apply(this, args);
    };
 
    unsafeWindow.WebSocket = function(url, protocols) {
        if (shouldBlockUrl(url)) {
            log('Blocking WebSocket connection to', url);
            const fakeWS = {
                url: url, protocol: protocols && protocols.length > 0 ? protocols[0] : '',
                readyState: 3, bufferedAmount: 0, extensions: '', binaryType: 'blob',
                CONNECTING: 0, OPEN: 1, CLOSING: 2, CLOSED: 3,
                send: function() { log('Fake WebSocket: send called on blocked WS'); return false; },
                close: function(code, reason) {
                    log('Fake WebSocket: close called on blocked WS', code, reason);
                    this.readyState = this.CLOSING;
                    setTimeout(() => {
                        this.readyState = this.CLOSED;
                        const closeEvent = new CloseEvent('close', { code: code || 1006, reason: reason || "Connection blocked", wasClean: false });
                        if (typeof this.onclose === 'function') this.onclose(closeEvent);
                        try { this.dispatchEvent(closeEvent); } catch(e) {warn('Error dispatching close on fake WS', e);}
                    }, 0);
                },
                onopen: null, onclose: null, onerror: null, onmessage: null,
                _listeners: {},
                addEventListener: function(type, listener) {
                    if (!this._listeners[type]) this._listeners[type] = [];
                    this._listeners[type].push(listener);
                },
                removeEventListener: function(type, listener) {
                    if (!this._listeners[type]) return;
                    this._listeners[type] = this._listeners[type].filter(l => l !== listener);
                },
                dispatchEvent: function(event) {
                    if (!this._listeners[event.type]) return true;
                    this._listeners[event.type].forEach(cb => (typeof cb === 'function' ? cb.call(this, event) : cb.handleEvent(event)));
                    return !event.defaultPrevented;
                }
            };
            setTimeout(() => {
                const errorEvent = new Event('error');
                if (typeof fakeWS.onerror === 'function') fakeWS.onerror(errorEvent);
                try { fakeWS.dispatchEvent(errorEvent); } catch(e) {warn('Error dispatching error on fake WS', e);}
                const closeEvent = new CloseEvent('close', { code: 1006, reason: "WebSocket blocked by script", wasClean: false });
                if (typeof fakeWS.onclose === 'function') fakeWS.onclose(closeEvent);
                try { fakeWS.dispatchEvent(closeEvent); } catch(e) {warn('Error dispatching close on fake WS', e);}
                fakeWS.readyState = fakeWS.CLOSED;
            }, 5);
            return fakeWS;
        }
        const wsInstance = new originalWebSocket(url, protocols);
        return wsInstance;
    };
    // Ensure prototype and static constants are correctly set up if needed by Torn's code
    if (originalWebSocket) {
        unsafeWindow.WebSocket.prototype = originalWebSocket.prototype;
        unsafeWindow.WebSocket.CONNECTING = originalWebSocket.CONNECTING;
        unsafeWindow.WebSocket.OPEN = originalWebSocket.OPEN;
        unsafeWindow.WebSocket.CLOSING = originalWebSocket.CLOSING;
        unsafeWindow.WebSocket.CLOSED = originalWebSocket.CLOSED;
    }
 
 
    // --- UI Toggle Button ---
    function updateToggleButton() {
        const button = document.getElementById(TOGGLE_BUTTON_ID);
        if (!button) return;
 
        button.classList.remove('blocking-on', 'blocking-off', 'blocking-always');
        let ariaLabel = '';
 
        if (isCurrentPageAlwaysBlocked()) {
            button.classList.add('blocking-always'); // Orange
            ariaLabel = 'Page Always Blocked. Click to remove from always-block list.';
            button.setAttribute('aria-checked', 'true'); // Visually "on"
        } else if (isBlockingEnabled) {
            button.classList.add('blocking-on'); // Green
            ariaLabel = 'Global Chat Blocking: ON. Click to turn OFF. Long press to always-block this page.';
            button.setAttribute('aria-checked', 'true');
        } else {
            button.classList.add('blocking-off'); // Red
            ariaLabel = 'Global Chat Blocking: OFF. Click to turn ON. Long press to always-block this page.';
            button.setAttribute('aria-checked', 'false');
        }
        button.setAttribute('aria-label', ariaLabel);
        button.setAttribute('role', 'switch');
    }
 
    function showNotification(message, backgroundColor) {
        const notification = document.createElement('div');
        notification.textContent = message;
        Object.assign(notification.style, {
            position: 'fixed', bottom: '20px', left: '50%', transform: 'translateX(-50%)',
            backgroundColor: backgroundColor, color: 'white',
            padding: '10px 20px', borderRadius: '5px', zIndex: '10000',
            boxShadow: '0 2px 10px rgba(0,0,0,0.2)'
        });
        document.body.appendChild(notification);
        setTimeout(() => notification.remove(), 3500);
    }
 
    function addToggleButton() {
        if (document.getElementById(TOGGLE_BUTTON_ID)) return;
        const button = document.createElement('button');
        button.id = TOGGLE_BUTTON_ID;
 
        const handleLongPress = () => {
            isLongPressFlag = true; // Set flag
            const currentPageUrl = window.location.href;
            if (!alwaysBlockedPages.has(currentPageUrl)) {
                alwaysBlockedPages.add(currentPageUrl);
                localStorage.setItem(LOCAL_STORAGE_KEY_ALWAYS_BLOCK_PAGES, JSON.stringify(Array.from(alwaysBlockedPages)));
                log(`Long press: Added ${currentPageUrl} to always-block list.`);
                showNotification(`Page added to always-block list. Refresh for full effect.`, '#FFA500'); // Orange notification
                updateToggleButton();
                if (pageShouldHaveElementsHidden()) { // Check if blocking should now be active
                    blockChatElementsInDOM(); // Ensure DOM observer is active
                }
            } else {
                log(`Long press: ${currentPageUrl} is already always-blocked.`);
                // Optional: showNotification(`${currentPageUrl} is already always-blocked.`, '#FFA500');
            }
        };
 
        const clearLongPressTimer = () => {
            if (longPressTimer) clearTimeout(longPressTimer);
            longPressTimer = null;
        };
 
        // Mouse events
        button.addEventListener('mousedown', (e) => {
            if (e.button !== 0) return; // Only left click
            isLongPressFlag = false; // Reset flag
            clearLongPressTimer();
            longPressTimer = setTimeout(handleLongPress, LONG_PRESS_DURATION);
        });
        button.addEventListener('mouseup', (e) => {
            if (e.button !== 0) return;
            clearLongPressTimer();
            // Click event will handle logic if not a long press
        });
        button.addEventListener('mouseleave', clearLongPressTimer);
 
        // Touch events
        button.addEventListener('touchstart', (e) => {
            isLongPressFlag = false; // Reset flag
            clearLongPressTimer();
            longPressTimer = setTimeout(handleLongPress, LONG_PRESS_DURATION);
            // e.preventDefault(); // Could prevent scroll, be cautious
        }, { passive: true }); // Passive if not preventing default
 
        button.addEventListener('touchend', (e) => {
            clearLongPressTimer();
            if (isLongPressFlag) {
                e.preventDefault(); // Prevent click event firing after a long touch
            }
            // Click event will handle logic if not a long press
        });
        button.addEventListener('touchcancel', clearLongPressTimer);
 
 
        button.addEventListener('click', (e) => {
            if (isLongPressFlag) { // If flag is set, it was a long press; reset and ignore click
                isLongPressFlag = false;
                e.stopImmediatePropagation(); // Prevent other click listeners if any
                return;
            }
 
            const currentPageUrl = window.location.href;
            if (isCurrentPageAlwaysBlocked()) {
                // Click on ORANGE switch: Remove from always-block
                alwaysBlockedPages.delete(currentPageUrl);
                localStorage.setItem(LOCAL_STORAGE_KEY_ALWAYS_BLOCK_PAGES, JSON.stringify(Array.from(alwaysBlockedPages)));
                log(`Clicked to unblock always-blocked page: ${currentPageUrl}`);
                showNotification(`Page removed from always-block list. Refresh for full effect.`, isBlockingEnabled ? '#388e3c' : '#d32f2f');
                updateToggleButton();
                if (!pageShouldHaveElementsHidden() && domObserver) {
                    domObserver.disconnect();
                    log('DOM Observer disconnected as page is no longer effectively blocked.');
                }
            } else {
                // Click on GREEN/RED switch: Toggle global blocking
                isBlockingEnabled = !isBlockingEnabled;
                localStorage.setItem(LOCAL_STORAGE_KEY_GLOBAL_BLOCK, isBlockingEnabled.toString());
                log(`Toggled Global Resource Blocking: ${isBlockingEnabled ? 'ON' : 'OFF'}`);
                showNotification(`Global Resource Blocking ${isBlockingEnabled ? 'enabled' : 'disabled'}. Refresh for full effect.`, isBlockingEnabled ? '#388e3c' : '#d32f2f');
                updateToggleButton();
 
                if (pageShouldHaveElementsHidden()) {
                    blockChatElementsInDOM();
                } else {
                    if (domObserver) domObserver.disconnect();
                    log("Global Resource Blocking OFF and page not always-blocked. DOM Observer potentially stopped. Refresh to restore elements.");
                }
            }
        });
 
        if (document.body) {
             document.body.appendChild(button);
        } else {
            window.addEventListener('DOMContentLoaded', () => {
                if (document.body) document.body.appendChild(button);
            });
        }
        updateToggleButton(); // Initialize button state
    }
 
    // --- Debounce Utility ---
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }
 
    // --- Block elements in DOM (Primarily for chat UI) ---
    const chatSelectors = [
        'div[id*="chat"]', 'div[class*="chat"]', 'div[id*="sendbird"]', 'div[class*="sendbird"]',
        'iframe[src*="chat"]', 'iframe[src*="sendbird"]', '#chatRoot', '.chat-box',
        '.chat-container', '.chat-wrapper', '*[id*="chat-"]', '*[class*="chat-"]',
        '*[id*="-chat"]', '*[class*="-chat"]', '*[id*="sendbird-"]', '*[class*="sendbird-"]',
        'div[aria-label*="chat" i]', 'section[aria-label*="chat" i]'
    ];
 
    function hideMatchedElements() {
        if (!pageShouldHaveElementsHidden()) {
             log('DOM element hiding is OFF for this page. Previously hidden elements will remain hidden until refresh.');
             // We don't attempt to unhide, refresh is cleaner.
            return;
        }
        log('Scanning and hiding chat-related DOM elements...');
        chatSelectors.forEach(selector => {
            try {
                document.querySelectorAll(selector).forEach(el => {
                    if (el.id === TOGGLE_BUTTON_ID || el.closest(`#${TOGGLE_BUTTON_ID}`)) return;
                    if (el.style.display !== 'none') {
                        log('Hiding DOM element matching selector:', selector, el);
                        el.style.setProperty('display', 'none', 'important');
                        el.style.setProperty('visibility', 'hidden', 'important');
                        el.style.setProperty('opacity', '0', 'important');
                        el.style.setProperty('pointer-events', 'none', 'important');
                        el.dataset.tornChatBlocked = 'true';
                    }
                });
            } catch (e) {
                warn('Error applying selector:', selector, e.message);
            }
        });
    }
 
    const debouncedHideMatchedElements = debounce(hideMatchedElements, DEBOUNCE_DELAY);
 
    function blockChatElementsInDOM() {
        if (!pageShouldHaveElementsHidden()) {
            if (domObserver) {
                domObserver.disconnect();
                log('DOM Observer disconnected as blocking is not active for this page.');
            }
            return;
        }
        log('Actively scanning/hiding chat DOM elements. Ensuring DOM observer is running.');
        hideMatchedElements(); // Initial scan
 
        if (!domObserver || !domObserver.takeRecords().length) { // Check if observer exists and is active
            domObserver = new MutationObserver((mutations) => {
                if (!pageShouldHaveElementsHidden()) { // Re-check condition within observer callback
                    if (domObserver) domObserver.disconnect();
                    log('DOM Observer disconnected from within callback.');
                    return;
                }
                let needsRescan = false;
                for (const mutation of mutations) {
                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        for (const node of mutation.addedNodes) {
                            if (node.nodeType === Node.ELEMENT_NODE) {
                                if (node.id === TOGGLE_BUTTON_ID) continue;
                                if (chatSelectors.some(sel => node.matches && node.matches(sel)) ||
                                    (node.querySelector && node.querySelector(chatSelectors.join(',')))) {
                                    needsRescan = true; break;
                                }
                            }
                        }
                    } else if (mutation.type === 'attributes') {
                        if (mutation.target.nodeType === Node.ELEMENT_NODE &&
                            chatSelectors.some(sel => mutation.target.matches && mutation.target.matches(sel))) {
                            needsRescan = true;
                        }
                    }
                    if (needsRescan) break;
                }
                if (needsRescan) {
                    log('DOM mutation detected, queueing re-hide for chat elements.');
                    debouncedHideMatchedElements();
                }
            });
 
            const observeTarget = document.body || document.documentElement;
            if (observeTarget) {
                 domObserver.observe(observeTarget, { childList: true, subtree: true, attributes: true });
                 log('DOM Observer started for chat elements.');
            } else { // Fallback if body not ready, though @run-at document-start + DOMContentLoaded should handle most
                 window.addEventListener('DOMContentLoaded', () => {
                    const target = document.body || document.documentElement;
                    if (target && pageShouldHaveElementsHidden()) { // Check again before observing
                        domObserver.observe(target, { childList: true, subtree: true, attributes: true });
                        log('DOM Observer started after DOMContentLoaded for chat elements.');
                    } else if (!target) {
                        warn("Failed to start DOM Observer: No body or documentElement found post-DOMContentLoaded.");
                    }
                 });
            }
        } else {
            log('DOM Observer already running.');
        }
    }
 
    // --- Initialization ---
    function initialize() {
        log('Initializing Torn Chat Blocker...');
        addToggleButton(); // This will also call updateToggleButton
 
        if (pageShouldHaveElementsHidden()) {
            log('Initial state requires blocking. Activating DOM blocking.');
            blockChatElementsInDOM();
        } else {
            log('Initial state does not require blocking.');
        }
 
        // Attempt to nullify global chat-related variables
        // This runs slightly after script start to catch variables defined by Torn's early scripts
        setTimeout(() => {
            if (pageShouldHaveElementsHidden()) { // Only nuke if blocking is active
                try {
                    const targetVars = ['chat', 'Chat', 'SendBird', 'sendbird', 'sb', '_sendbird', 'SENDBIRD'];
                    targetVars.forEach(v => {
                        if (typeof unsafeWindow[v] !== 'undefined' && unsafeWindow[v] !== null) { // Check if not already null
                            log(`Attempting to nullify unsafeWindow.${v}`);
                            try {
                                Object.defineProperty(unsafeWindow, v, { value: null, writable: false, configurable: false });
                            } catch (e) {
                                warn(`Failed to Object.defineProperty ${v}, falling back to simple null. Error: ${e.message}`);
                                try {
                                    unsafeWindow[v] = null;
                                } catch (e2) {
                                    warn(`Failed to even assign null to unsafeWindow.${v}. Error: ${e2.message}`);
                                }
                            }
                        }
                    });
                } catch (e) {
                    warn('Error while trying to nuke JS variables.', e.message);
                }
            }
        }, 200); // Increased delay slightly
    }
 
    if (document.readyState === 'loading') {
        window.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }
 
})();