Automagically blur comments on X (Twitter) and adds a button to toggle between visible/blurred.
// ==UserScript==
// @name X/Twitter Comments Blurrifier
// @namespace http://tampermonkey.net/
// @version 7.0
// @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
// @grant GM_addStyle
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
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)
const X_GRAY_ICON = '#71767B'; // Default dark gray icon color on X
// Initial state: Comments are blurred
let isBlurred = true;
// --- SVG Definitions ---
// Blurred (Inactive state - Icon should be your X_BLUE color)
const blurredSvg = `
<svg xmlns="http://www.w3.org/2000/svg" height="22.5px" viewBox="0 -960 960 960" width="22.5px" style="fill:${X_BLUE}">
<path d="M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-134 0-244.5-72T61-462q-5-9-7.5-18.5T51-500q0-10 2.5-19.5T61-538q64-118 174.5-190T480-800q134 0 244.5 72T899-538q5 9 7.5 18.5T909-500q0 10-2.5 19.5T899-462q-64 118-174.5 190T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z"/>
</svg>`;
// Unblurred (Active state - Icon should be X_PINK color)
const unblurredSvg = `
<svg xmlns="http://www.w3.org/2000/svg" height="22.5px" viewBox="0 -960 960 960" width="22.5px" style="fill:${X_PINK}">
<path d="M607-627q29 29 42.5 66t9.5 76q0 15-11 25.5T622-449q-15 0-25.5-10.5T586-485q5-26-3-50t-25-41q-17-17-41-26t-51-4q-15 0-25.5-11T430-643q0-15 10.5-25.5T466-679q38-4 75 9.5t66 42.5Zm-127-93q-19 0-37 1.5t-36 5.5q-17 3-30.5-5T358-742q-5-16 3.5-31t24.5-18q23-5 46.5-7t47.5-2q137 0 250.5 72T904-534q4 8 6 16.5t2 17.5q0 9-1.5 17.5T905-466q-18 40-44.5 75T802-327q-12 11-28 9t-26-16q-10-14-8.5-30.5T753-392q24-23 44-50t35-58q-50-101-144.5-160.5T480-720Zm0 520q-134 0-245-72.5T60-463q-5-8-7.5-17.5T50-500q0-10 2-19t7-18q20-40 46.5-76.5T166-680l-83-84q-11-12-10.5-28.5T84-820q11-11 28-11t28 11l680 680q11 11 11.5 27.5T820-84q-11 11-28 11t-28-11L624-222q-35 11-71 16.5t-73 5.5ZM222-624q-29 26-53 57t-41 67q50 101 144.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z"/>
</svg>`;
// 2. Inject Base CSS for Blurring and Hover State
GM_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; /* Ensures it sits below the icon itself */
}
/* The SVG is a direct child of the icon container */
#${ICON_ID} svg {
z-index: 2; /* Ensures the icon sits above the pseudo-element background */
}
/* --- HOVER STATE LOGIC --- */
/* 1. Default (Blurred/Inactive) State Hover: Apply PINK hover */
/* If the body does NOT have the active class (comments are blurred), apply pink hover. */
body:not(.${ACTIVE_CLASS}) div[id="${ICON_ID}"]:hover {
color: ${X_PINK}; /* Changes the icon color to pink */
}
body:not(.${ACTIVE_CLASS}) div[id="${ICON_ID}"]:hover::before {
background-color: rgba(29, 161, 242, 0.1); /* Low-opacity X pink background (f91880) */
}
/* 2. Active (Unblurred) State Hover: Apply BLUE hover */
/* If the body HAS the active class (comments are unblurred), apply blue hover. */
body.${ACTIVE_CLASS} div[id="${ICON_ID}"]:hover {
color: ${X_BLUE}; /* Changes the icon color to blue */
}
body.${ACTIVE_CLASS} div[id="${ICON_ID}"]:hover::before {
background-color: rgba(249, 24, 128, 0.1); /* Low-opacity X blue 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;
// Re-check the body class and set initial SVG
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();
})();