Greasy Fork

Greasy Fork is available in English.

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       http://greasyfork.icu/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 results, returns an array of the resulting elements **/
    function xPathQuerySnapshot(path) {
        const queryResult = document.evaluate(path, document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
        return [...Array(queryResult.snapshotLength).keys()] // [0,1,2… N-1]
            .map(i => queryResult.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
})();