🏠 Home 

Remove ads and promoted tweets on Twitter

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
})();