-
-
Save disinfeqt/f306eee5fbfdb086fc25a030f2d3f044 to your computer and use it in GitHub Desktop.
Not Interested for Arc Boost
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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(); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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