Removes ads and promoted tweets on Twitter, as well as the large bold "Promoted Tweet" headings
// ==UserScript== // @name Remove ads and promoted tweets on Twitter // @namespace http://tampermonkey.net/ // @version 1.2 // @description Removes ads and promoted tweets on Twitter, as well as the large bold "Promoted Tweet" headings // @author https://greasyfork.org/en/users/728793-keyboard-shortcuts // @match https://twitter.com/* // @match https://*.twitter.com/* // @match https://x.com/* // @match https://*.x.com/* // @icon https://www.google.com/s2/favicons?sz=128&domain=twitter.com // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; /***************** CONFIGURATION STARTS HERE *****************/ const FIND_AND_REMOVE_FREQUENCY_MS = 500; // how often to search for promoted tweets to remove them, in milliseconds (by default 500, to do it twice a second) const LOG_REMOVALS = false; // set to true if you want the removals to be reported in the web developer console (not usually visible unless you open it yourself) /****************** CONFIGURATION ENDS HERE ******************/ const PROMOTED_ARTICLE_PATH = '//span[text()="Promoted"]/ancestor::article[@aria-labelledby]'; // xPath query to find promoted tweets, specifically their <article> DOM node const PROMOTED_TWEET_HEADING_PATH = '//span[text()="Promoted Tweet"]/parent::div/parent::h2/parent::div'; // Finds "<div><h2><div><span>Promoted tweet</span>…", selects the outer <div>. const PROMOTED_TREND_PATH = '//span[starts-with(text(),"Promoted by")]/ancestor::div[@aria-labelledby]'; // xPath query to find promoted trending topics, specifically their <div> DOM node const ADS_ARTICLE_PATH = '//span[text()="Ad"]/ancestor::article[@aria-labelledby]'; // xPath query to find advertising tweets, specifically their <article> DOM node /** * For an <article> element, take its list of IDs in aria-labelledby and return the DOM nodes with these IDs * e.g. for <article aria-labelledby="foo bar">, return the nodes with id="foo" and id="bar". */ function getLabelNodes(article) { const labelledBy = article.getAttribute('aria-labelledby'); // space-separated node IDs const selector = labelledBy.split(' ').map(id => '#' + id).join(', '); // convert to a selector like '#foo, #bar' return Array.from(document.querySelectorAll(selector)); // returns the list of nodes found with these IDs } /** Returns whether one of the "label nodes" this article is labeled by has the text "Promoted" as its contents **/ function isPromotedArticle(article) { return getLabelNodes(article) .some(node => ('' + node.textContent).toLowerCase() === 'promoted'); } /** Returns whether the article contains a span with only the text "Ad" **/ function isAdvertisement(article) { return Array.from(article.querySelectorAll('span')) .some(node => ('' + node.textContent).toLowerCase() === 'ad'); } /** Runs an XPath query with a snapshot of the r###lts, returns an array of the r###lting elements **/ function xPathQuerySnapshot(path) { const queryR###lt = document.evaluate(path, document, null, XPathR###lt.UNORDERED_NODE_SNAPSHOT_TYPE, null); return [...Array(queryR###lt.snapshotLength).keys()] // [0,1,2… N-1] .map(i => queryR###lt.snapshotItem(i)); } /** Remove element and log reason, if needed **/ function removeAndMaybeLog(element, logMessage) { if (LOG_REMOVALS) { console.log('Removing ' + logMessage); } element.style.display = 'none'; } /** Finds and removes promoted tweets and the headings above these tweets **/ function findAndRemoveUnwantedTweets() { xPathQuerySnapshot(PROMOTED_ARTICLE_PATH) .filter(article => isPromotedArticle(article)) .forEach(article => removeAndMaybeLog(article, 'promoted tweet:' + article.textContent)); xPathQuerySnapshot(ADS_ARTICLE_PATH) .filter(article => isAdvertisement(article)) .forEach(article => removeAndMaybeLog(article, 'advertisement:' + article.textContent)); xPathQuerySnapshot(PROMOTED_TWEET_HEADING_PATH) .forEach(div => removeAndMaybeLog(div, 'promoted tweet heading')); xPathQuerySnapshot(PROMOTED_TREND_PATH) .forEach(div => removeAndMaybeLog(div, 'promoted trending topic:' + div.textContent)); } findAndRemoveUnwantedTweets(); // do it once when the page loads setInterval(findAndRemoveUnwantedTweets, FIND_AND_REMOVE_FREQUENCY_MS); // and then at regular intervals after that })();