Hide Twitter Ads

Hide ads on Twitter

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Hide Twitter Ads
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Hide ads on Twitter
// @author       Yeky
// @match        https://x.com/*
// @grant        none
// @name:en    Hide Twitter Ads
// @name:zh    隐藏推特广告
// @description  这个油猴脚本隐藏Twitter上的广告、趋势和用户推荐,提供更干净的浏览体验。
// @description:en  This Tampermonkey script hides ads, trends, and user recommendations on Twitter for a cleaner browsing experience.
// @icon    https://lh3.googleusercontent.com/RhFY2tkBB9MSrgCTDW0JdYnB51bJ_QmEPZRHVISanz34PWFA4CSCwW75m34sR7Oynl38odzKabuCIrWHB44akhHU=s60
// @license    All Rights Reserved
// ==/UserScript==

(function() {
    'use strict';

    var adsHidden = 0;
    var adSelector = "div[data-testid=placementTracking]";
    var trendSelector = "div[data-testid=trend]";
    var userSelector = "div[data-testid=UserCell]";
    var articleSelector = "article[data-testid=tweet]";

    var sponsoredSvgPath = 'M20.75 2H3.25C2.007 2 1 3.007 1 4.25v15.5C1 20.993 2.007 22 3.25 22h17.5c1.243 0 2.25-1.007 2.25-2.25V4.25C23 3.007 21.993 2 20.75 2zM17.5 13.504c0 .483-.392.875-.875.875s-.875-.393-.875-.876V9.967l-7.547 7.546c-.17.17-.395.256-.62.256s-.447-.086-.618-.257c-.342-.342-.342-.896 0-1.237l7.547-7.547h-3.54c-.482 0-.874-.393-.874-.876s.392-.875.875-.875h5.65c.483 0 .875.39.875.874v5.65z';
    var sponsoredBySvgPath = 'M19.498 3h-15c-1.381 0-2.5 1.12-2.5 2.5v13c0 1.38 1.119 2.5 2.5 2.5h15c1.381 0 2.5-1.12 2.5-2.5v-13c0-1.38-1.119-2.5-2.5-2.5zm-3.502 12h-2v-3.59l-5.293 5.3-1.414-1.42L12.581 10H8.996V8h7v7z';
    var youMightLikeSvgPath = 'M12 1.75c-5.11 0-9.25 4.14-9.25 9.25 0 4.77 3.61 8.7 8.25 9.2v2.96l1.15-.17c1.88-.29 4.11-1.56 5.87-3.5 1.79-1.96 3.17-4.69 3.23-7.97.09-5.54-4.14-9.77-9.25-9.77zM13 14H9v-2h4v2zm2-4H9V8h6v2z';
    var adsSvgPath = 'M19.498 3h-15c-1.381 0-2.5 1.12-2.5 2.5v13c0 1.38 1.119 2.5 2.5 2.5h15c1.381 0 2.5-1.12 2.5-2.5v-13c0-1.38-1.119-2.5-2.5-2.5zm-3.502 12h-2v-3.59l-5.293 5.3-1.414-1.42L12.581 10H8.996V8h7v7z';
    var peopleFollowSvgPath = 'M17.863 13.44c1.477 1.58 2.366 3.8 2.632 6.46l.11 1.1H3.395l.11-1.1c.266-2.66 1.155-4.88 2.632-6.46C7.627 11.85 9.648 11 12 11s4.373.85 5.863 2.44zM12 2C9.791 2 8 3.79 8 6s1.791 4 4 4 4-1.79 4-4-1.791-4-4-4z';
    var xAd = '>Ad<'; // TODO: add more languages; appears to only be used for English accounts as of 2023-08-03
    var removePeopleToFollow = false; // set to 'true' if you want these suggestions removed, however note this also deletes some tweet replies
    const promotedTweetTextSet = new Set(['Promoted Tweet', 'プロモツイート']);

    function getAds() {
      return Array.from(document.querySelectorAll('div')).filter(function(el) {
        var filteredAd;

        if (el.innerHTML.includes(sponsoredSvgPath)) {
          filteredAd = el;
        } else if (el.innerHTML.includes(sponsoredBySvgPath)) {
          filteredAd = el;
        } else if (el.innerHTML.includes(youMightLikeSvgPath)) {
          filteredAd = el;
        } else if (el.innerHTML.includes(adsSvgPath)) {
          filteredAd = el;
        } else if (removePeopleToFollow && el.innerHTML.includes(peopleFollowSvgPath)) {
          filteredAd = el;
        } else if (el.innerHTML.includes(xAd)) {
          filteredAd = el;
        } else if (promotedTweetTextSet.has(el.innerText)) { // TODO: bring back multi-lingual support from git history
          filteredAd = el;
        }

        return filteredAd;
      })
    }

    function hideAd(ad) {
      if (ad.closest(adSelector) !== null) { // Promoted tweets
        ad.closest(adSelector).remove();
        adsHidden += 1;
      } else if (ad.closest(trendSelector) !== null) {
        ad.closest(trendSelector).remove();
        adsHidden += 1;
      } else if (ad.closest(userSelector) !== null) {
        ad.closest(userSelector).remove();
        adsHidden += 1;
      } else if (ad.closest(articleSelector) !== null) {
        ad.closest(articleSelector).remove();
        adsHidden += 1;
      } else if (promotedTweetTextSet.has(ad.innerText)) {
        ad.remove();
        adsHidden += 1;
      }

      console.log('X ads hidden: ', adsHidden.toString());
    }

    function getAndHideAds() {
      getAds().forEach(hideAd)
    }

    // hide ads on page load
    window.addEventListener('load', () => getAndHideAds());

    // oftentimes, tweets render after onload. LCP should catch them.
    new PerformanceObserver((entryList) => {
      getAndHideAds();
    }).observe({type: 'largest-contentful-paint', buffered: true});

    // re-check as user scrolls
    window.addEventListener('scroll', () => getAndHideAds());

    // re-check as user scrolls tweet sidebar (exists when image is opened)
    var sidebarExists = setInterval(function() {
      let timelines = document.querySelectorAll("[aria-label='Timeline: Conversation']");

      if (timelines.length == 2) {
        let tweetSidebar = document.querySelectorAll("[aria-label='Timeline: Conversation']")[0].parentElement.parentElement;
        tweetSidebar.addEventListener('scroll', () => getAndHideAds());
      }
    }, 500);
})();