Hide Twitter Ads

Hide ads on Twitter

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
})();