// ==UserScript==
// @name X/Twitter Comments Blurrifier
// @namespace http://tampermonkey.net/
// @version 7.1
// @description Automagically blur comments on X (Twitter) and adds a button to toggle between visible/blurred.
// @author nereids
// @license MIT
// @match https://twitter.com/*
// @match https://x.com/*
// @icon https://icons.duckduckgo.com/ip3/x.com.ico
// @run-at document-start
// @downloadURL https://update.greasyfork.icu/scripts/554304/XTwitter%20Comments%20Blurrifier.user.js
// @updateURL https://update.greasyfork.icu/scripts/554304/XTwitter%20Comments%20Blurrifier.meta.js
// ==/UserScript==
(function() {
'use strict';
// --- CROSS-BROWSER CSS INJECTION FIX ---
function addStyle(css) {
const style = document.createElement('style');
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
}
// --- END FIX ---
const CONVERSATION_SELECTOR = 'div[aria-label="Timeline: Conversation"]';
const ACTION_BAR_SELECTOR = 'article div[role="group"]';
const ICON_ID = 'toggleBlurIcon';
const ACTIVE_CLASS = 'blur-disabled';
// --- Custom Colors ---
const X_BLUE = '#1d9bf0'; // For Active/Unblurred state
const X_PINK = '#f91880'; // For Inactive/Blurred state (your custom color)
// Initial state: Comments are blurred
let isBlurred = true;
// --- SVG Definitions (Using your provided sizes and colors) ---
const blurredSvg = `
`;
const unblurredSvg = `
`;
// 2. Inject Base CSS for Blurring and Hover State
addStyle(`
/* Blur and Alignment Styles */
${CONVERSATION_SELECTOR} div[data-testid="cellInnerDiv"]:not(:first-child) {
filter: blur(4px) !important;
transition: filter 0.2s ease-in-out;
pointer-events: none !important;
user-select: none !important;
}
body.${ACTIVE_CLASS} ${CONVERSATION_SELECTOR} div[data-testid="cellInnerDiv"]:not(:first-child) {
filter: none !important;
pointer-events: auto !important;
user-select: auto !important;
}
/* ICON WRAPPER STYLING */
#${ICON_ID} {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 46px;
margin: 0 0 0 15px !important;
padding: 0 !important;
transition: background-color 0.2s ease-in-out;
}
/* Pseudo-element for circular background */
div[id="${ICON_ID}"]::before {
content: '';
position: absolute;
width: 38.5px;
height: 38.5px;
border-radius: 50%;
transition: background-color 0.2s ease-in-out;
z-index: 1;
}
/* The SVG is a direct child of the icon container */
#${ICON_ID} svg {
z-index: 2;
}
/* --- HOVER STATE LOGIC --- */
/* 1. Default (Blurred/Inactive, Icon Blue) State Hover: Apply BLUE hover */
body:not(.${ACTIVE_CLASS}) div[id="${ICON_ID}"]:hover {
color: ${X_BLUE}; /* Changes the icon color to blue */
}
body:not(.${ACTIVE_CLASS}) div[id="${ICON_ID}"]:hover::before {
background-color: rgba(29, 161, 242, 0.1); /* Low-opacity X blue background */
}
/* 2. Active (Unblurred, Icon Pink) State Hover: Apply PINK hover */
body.${ACTIVE_CLASS} div[id="${ICON_ID}"]:hover {
color: ${X_PINK}; /* Changes the icon color to pink */
}
body.${ACTIVE_CLASS} div[id="${ICON_ID}"]:hover::before {
background-color: rgba(249, 24, 128, 0.1); /* Low-opacity X pink background */
}
/* The cloned button wrapper needs fixed dimensions for consistent alignment */
#${ICON_ID}:not(:first-child) {
width: 38.5px;
height: 38.5px;
}
`);
// 3. Logic to inject the icon and attach the event handler (SPA logic)
function injectToggleIcon() {
const actionBar = document.querySelector(ACTION_BAR_SELECTOR);
if (actionBar && !document.getElementById(ICON_ID)) {
const iconContainer = document.createElement('div');
iconContainer.id = ICON_ID;
isBlurred = !document.body.classList.contains(ACTIVE_CLASS);
iconContainer.innerHTML = isBlurred ? blurredSvg : unblurredSvg;
const existingButton = actionBar.children[0];
if (!existingButton) return;
const targetButton = existingButton.querySelector('svg') ? existingButton : actionBar.children[1];
const buttonWrapper = targetButton.cloneNode(false);
buttonWrapper.innerHTML = '';
buttonWrapper.appendChild(iconContainer);
actionBar.appendChild(buttonWrapper);
// Click Handler: Toggle state and update icon
iconContainer.addEventListener('click', function(e) {
e.stopPropagation();
isBlurred = !isBlurred;
document.body.classList.toggle(ACTIVE_CLASS);
// Update SVG
iconContainer.innerHTML = isBlurred ? blurredSvg : unblurredSvg;
});
}
}
// --- MutationObserver Setup (SPA logic) ---
const observerCallback = (mutations) => {
for (const mutation of mutations) {
if (mutation.addedNodes.length) {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1 && (node.matches(CONVERSATION_SELECTOR) || node.querySelector(CONVERSATION_SELECTOR))) {
setTimeout(injectToggleIcon, 50);
return;
}
}
}
}
};
const observer = new MutationObserver(observerCallback);
const targetNode = document.body;
const config = { childList: true, subtree: true };
observer.observe(targetNode, config);
// Initial call
injectToggleIcon();
})();