Skip to content

Instantly share code, notes, and snippets.

@disinfeqt
Last active March 15, 2025 06:41
Show Gist options
  • Save disinfeqt/f306eee5fbfdb086fc25a030f2d3f044 to your computer and use it in GitHub Desktop.
Save disinfeqt/f306eee5fbfdb086fc25a030f2d3f044 to your computer and use it in GitHub Desktop.
Not Interested for Arc Boost
// ==UserScript==
// @name X.com Not Interested Shortcut
// @namespace https://gist.github.com/disinfeqt/f306eee5fbfdb086fc25a030f2d3f044
// @version 1.6.2
// @description Adds ✕ button and 'X' shortcut to quickly mark posts as Not Interested
// @match https://x.com/home
// @grant none
// ==/UserScript==
(function () {
"use strict";
const processedTweets = new WeakSet();
let hoveredTweet = null;
let isOnForYouTab = false;
// Add pattern detection configuration
const usernamePatterns = [
/0x/i, // Matches "0x" anywhere in text (case-insensitive)
/互fo/i, // Matches "互fo" anywhere in text (case-insensitive)
/\.eth/i, // Matches ".eth" anywhere in text (case-insensitive)
/互关/i, // Matches "互关" anywhere in text (case-insensitive)
/互赞/i, // Matches "互赞" anywhere in text (case-insensitive)
/[\u3040-\u309F\u30A0-\u30FF]/, // Matches only Japanese characters (hiragana and katakana)
];
function checkUsernamePatterns(tweetElement) {
// Get the username element
const usernameElement = tweetElement.querySelector(
'[data-testid="User-Name"]'
);
if (!usernameElement) return false;
// Get both display name and username using stable selectors
const displayName =
usernameElement.querySelector('div[dir="ltr"] div[dir="ltr"] span')
?.textContent || "";
// Find the username by looking for the link containing @ symbol
const usernameLink = Array.from(
usernameElement.querySelectorAll('a[role="link"]')
).find((link) => link.textContent.startsWith("@"));
const username = usernameLink?.textContent || "";
// Check both display name and username against patterns
for (const pattern of usernamePatterns) {
if (pattern.test(displayName)) {
console.log(
"[X Script] Pattern matched display name:",
pattern,
"in:",
displayName
);
return true;
}
if (pattern.test(username)) {
console.log(
"[X Script] Pattern matched username:",
pattern,
"in:",
username
);
return true;
}
}
return false;
}
function waitForElement(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(() => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
setTimeout(() => {
observer.disconnect();
reject(new Error(`Timeout waiting for ${selector}`));
}, timeout);
});
}
function waitForElementWithText(selector, textContent, timeout = 5000) {
return new Promise((resolve, reject) => {
const checkForElement = () => {
const elements = document.querySelectorAll(selector);
for (const element of elements) {
const elementText = element?.textContent || "";
if (
elementText.trim().toLowerCase().includes(textContent.toLowerCase())
) {
return element;
}
}
return null;
};
// Check immediately first
const element = checkForElement();
if (element) {
return resolve(element);
}
// If not found, set up observer
const observer = new MutationObserver((mutations, obs) => {
const element = checkForElement();
if (element) {
obs.disconnect();
resolve(element);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
setTimeout(() => {
observer.disconnect();
reject(
new Error(
`Element with text "${textContent}" not found within timeout.`
)
);
}, timeout);
});
}
function checkForYouTab() {
// Check URL first
if (!window.location.pathname.match(/^\/home$/)) {
// console.log("[X Script] Not on home page");
return false;
}
// Look for the For you tab with the correct structure
const tabElements = document.querySelectorAll('[role="tab"]');
// console.log("[X Script] Found tab elements:", tabElements.length);
for (const tab of tabElements) {
// Find the span containing "For you" text
const span = tab.querySelector("span.css-1jxf684");
if (!span) continue;
const tabText = span.textContent.trim();
// console.log("[X Script] Checking tab:", tabText);
if (tabText === "For you") {
const isSelected = tab.getAttribute("aria-selected") === "true";
// console.log("[X Script] Found For you tab, selected:", isSelected);
return isSelected;
}
}
return false;
}
function updateForYouStatus() {
const newStatus = checkForYouTab();
if (newStatus !== isOnForYouTab) {
// console.log("[X Script] Tab status changed:", newStatus);
isOnForYouTab = newStatus;
// If we switched to For you tab, process existing tweets
if (isOnForYouTab) {
// console.log("[X Script] Processing existing tweets on tab change");
document.querySelectorAll("article").forEach(addCustomButton);
}
}
}
// Watch for tab changes and URL changes
function setupTabObserver() {
// Watch for tab attribute changes
const tabObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (
mutation.target.getAttribute("role") === "tab" ||
mutation.target.closest('[role="tab"]')
) {
// console.log("[X Script] Tab change detected");
updateForYouStatus();
break;
}
}
});
tabObserver.observe(document.body, {
attributes: true,
subtree: true,
attributeFilter: ["aria-selected", "class"],
});
// Watch for URL changes
let lastUrl = location.href;
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
// console.log("[X Script] URL changed, checking tab status");
setTimeout(updateForYouStatus, 500); // Give the page time to update
}
}).observe(document, { subtree: true, childList: true });
}
document.addEventListener("keydown", handleKeyPress);
function setupTweetHoverTracking(tweetElement) {
tweetElement.addEventListener("mouseenter", () => {
if (isOnForYouTab) {
hoveredTweet = tweetElement;
}
});
tweetElement.addEventListener("mouseleave", () => {
if (hoveredTweet === tweetElement) {
hoveredTweet = null;
}
});
}
function handleKeyPress(event) {
if (
isOnForYouTab &&
(event.key === "x" || event.key === "X") &&
hoveredTweet &&
!event.ctrlKey &&
!event.metaKey &&
!event.altKey &&
!event.target.matches('input, textarea, [contenteditable="true"]') // Don't trigger in input fields
) {
event.preventDefault();
clickNotInterestedOption(hoveredTweet);
}
}
function waitForMoreButton(tweetElement, timeout = 5000) {
return new Promise((resolve, reject) => {
const start = Date.now();
const interval = setInterval(() => {
const moreButton = tweetElement.querySelector('[data-testid="caret"]');
if (moreButton) {
clearInterval(interval);
resolve(moreButton);
} else if (Date.now() - start > timeout) {
clearInterval(interval);
reject(new Error("More button not found within timeout."));
}
}, 200);
});
}
/**
* Executes the Not Interested action sequence:
* 1. Click More
* 2. Click "Not interested in this post"
* 3. Click "This post isn't relevant"
*/
function clickNotInterestedOption(tweetElement) {
waitForMoreButton(tweetElement)
.then((moreButton) => {
moreButton.click();
return waitForElementWithText(
'div[role="menuitem"]',
"Not interested in this post",
500
);
})
.then((notInterestedButton) => {
notInterestedButton.click();
return new Promise((resolve) => setTimeout(resolve, 500));
})
.then(() => {
return waitForElementWithText(
'[role="menuitem"], [role="button"]',
"relevant",
500
);
})
.then((relevantButton) => {
relevantButton.click();
})
.catch((err) => {
console.error("[X Script] Error in clickNotInterestedOption:", err);
// Try to close any open menus
const closeButton = document.querySelector(
'[data-testid="app-bar-close"]'
);
if (closeButton) closeButton.click();
});
}
/**
* Adds ✕ button next to More button for quick Not Interested action
*/
function addCustomButton(tweetElement) {
if (!isOnForYouTab) {
// console.log("[X Script] Not on For you tab, skipping button add");
return;
}
if (processedTweets.has(tweetElement)) {
// console.log("[X Script] Tweet already processed, skipping");
return;
}
// console.log("[X Script] Adding button to tweet");
processedTweets.add(tweetElement);
// Check for username patterns and auto-mark as not interested if matched
if (checkUsernamePatterns(tweetElement)) {
// console.log("[X Script] Pattern matched, auto-marking as not interested");
clickNotInterestedOption(tweetElement);
return;
}
setupTweetHoverTracking(tweetElement);
const button = document.createElement("button");
button.textContent = "✕";
button.title = "Not Interested (or press 'X' while hovering)";
button.className = "custom-not-interested-btn";
button.style.marginRight = "8px";
button.style.padding = "2px 4px";
button.style.background = "none";
button.style.color = "#657786";
button.style.border = "none";
button.style.cursor = "pointer";
button.style.fontSize = "20px";
button.style.lineHeight = "1";
button.style.display = "inline-flex";
button.style.alignItems = "center";
button.style.justifyContent = "center";
button.addEventListener("click", (e) => {
e.stopPropagation();
clickNotInterestedOption(tweetElement);
});
waitForMoreButton(tweetElement)
.then((moreButton) => {
// console.log("[X Script] Found More button, adding X button");
if (moreButton.parentElement) {
moreButton.parentElement.style.display = "inline-flex";
}
moreButton.insertAdjacentElement("beforebegin", button);
})
.catch((err) => {
console.error("[X Script] Error adding button:", err);
});
}
/**
* Uses a MutationObserver to detect new tweet elements and adds the custom button.
*/
function initObserver() {
// Wait for body to be available
if (!document.body) {
// console.log("[X Script] Body not available, retrying...");
setTimeout(initObserver, 100);
return;
}
// console.log("[X Script] Initializing observers");
updateForYouStatus(); // Initial check
setupTabObserver(); // Setup tab change observer
const tweetObserver = new MutationObserver((mutations) => {
if (!isOnForYouTab) return;
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Look for tweet articles with the specific structure
const articles = node.matches('[data-testid="tweet"]')
? [node]
: node.querySelectorAll('[data-testid="tweet"]');
articles.forEach((article) => {
// console.log("[X Script] New tweet found");
addCustomButton(article);
});
}
});
});
});
tweetObserver.observe(document.body, { childList: true, subtree: true });
// Process tweets already present on the page if we're on For you tab
if (isOnForYouTab) {
// console.log("[X Script] Processing initial tweets");
document
.querySelectorAll('[data-testid="tweet"]')
.forEach(addCustomButton);
}
}
async function initScript() {
try {
// Wait for the tablist to be present
// console.log("[X Script] Waiting for tablist to load...");
await waitForElement('[role="tablist"]');
// console.log("[X Script] Tablist found, initializing script");
// Initialize observers
initObserver();
} catch (error) {
console.error("[X Script] Error initializing:", error);
}
}
// Initialize everything once DOM is ready
if (document.readyState === "loading") {
// console.log("[X Script] Waiting for DOMContentLoaded");
document.addEventListener("DOMContentLoaded", initScript);
} else {
// console.log("[X Script] DOM already ready, initializing");
initScript();
}
})();
@disinfeqt
Copy link
Author

disinfeqt commented Mar 3, 2025

v1.5: Only works on For you tab
v1.5.1: Don't trigger in input fields
v1.6: Added filter based on name patterns
v1.6.1: Only work on home page

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment