X Reading Enhancer

Improve x.com reading by toggling side columns, ads, and media defaults.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         X Reading Enhancer
// @name:zh-CN   X 阅读增强
// @namespace    https://github.com/local/x-reading-enhancer
// @version      0.4.1
// @description  Improve x.com reading by toggling side columns, ads, and media defaults.
// @description:zh-CN 优化 x.com / twitter.com 阅读体验:隐藏左右栏、广告,控制媒体默认展示,并提供快捷键和可拖动悬浮面板。
// @author       Codex
// @match        https://x.com/*
// @match        https://twitter.com/*
// @license      MIT
// @supportURL   https://github.com/Skylerliutian/x-reading-enhancer/issues
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-start
// ==/UserScript==

(function () {
  "use strict";

  const STORAGE_PREFIX = "x-reading-enhancer.";
  const DEFAULTS = {
    leftVisible: true,
    rightVisible: true,
    hideAds: true,
    mediaVisible: true,
    panelOpen: false,
    panelPosition: null,
  };
  const DRAG_MARGIN = 8;
  const DRAG_CLICK_TOLERANCE = 4;
  const PANEL_OFFSET = 22;
  const SHORTCUTS = {
    A: "hideAds",
    L: "leftVisible",
    M: "mediaVisible",
    R: "rightVisible",
  };

  const AD_PATTERNS = [
    /^ad$/i,
    /^promoted$/i,
    /^promoted by\b/i,
    /^sponsored$/i,
    /^sponsored by\b/i,
    /^广告$/,
    /^廣告$/,
    /^推广$/,
    /^推廣$/,
    /^赞助$/,
    /^贊助$/,
    /^プロモーション$/,
    /^広告$/,
    /^プロモーション広告$/,
    /^gesponsert$/i,
    /^anzeige$/i,
    /^sponsorise$/i,
    /^sponsorise par\b/i,
    /^promocionado$/i,
    /^promovido$/i,
    /^promosso$/i,
    /^annuncio$/i,
    /^reklama$/i,
    /^реклама$/i,
  ];

  const MEDIA_SELECTORS = [
    '[data-testid="tweetPhoto"]',
    '[data-testid="videoPlayer"]',
    '[data-testid="videoComponent"]',
    '[data-testid="gifPlayer"]',
    '[data-testid="playButton"]',
    'a[href*="/photo/"]',
    'a[href*="/video/"]',
    'img[src*="pbs.twimg.com/media"]',
    'img[src*="pbs.twimg.com/amplify_video_thumb"]',
    "video",
  ].join(",");
  const MEDIA_BLOCK_SELECTORS = [
    '[data-testid="tweetPhoto"]',
    '[data-testid="videoPlayer"]',
    '[data-testid="videoComponent"]',
    '[data-testid="gifPlayer"]',
    'a[href*="/photo/"]',
    'a[href*="/video/"]',
  ].join(",");

  const settings = loadSettings();
  let observer = null;
  let scanQueued = false;
  let mediaToggleListenerInstalled = false;

  installStyles();
  applyRootClasses();
  onReady(() => {
    ensurePanel();
    setupMediaToggleListener();
    scanPage();
    startObserver();
  });

  function loadSettings() {
    return Object.fromEntries(
      Object.entries(DEFAULTS).map(([key, fallback]) => [key, readSetting(key, fallback)]),
    );
  }

  function readSetting(key, fallback) {
    const storageKey = STORAGE_PREFIX + key;

    try {
      if (typeof GM_getValue === "function") {
        return GM_getValue(storageKey, fallback);
      }
    } catch (_) {
      // Fall through to localStorage.
    }

    try {
      const stored = window.localStorage.getItem(storageKey);
      return stored == null ? fallback : JSON.parse(stored);
    } catch (_) {
      return fallback;
    }
  }

  function writeSetting(key, value) {
    settings[key] = value;
    const storageKey = STORAGE_PREFIX + key;

    try {
      if (typeof GM_setValue === "function") {
        GM_setValue(storageKey, value);
        return;
      }
    } catch (_) {
      // Fall through to localStorage.
    }

    try {
      window.localStorage.setItem(storageKey, JSON.stringify(value));
    } catch (_) {
      // Ignore storage failures; current-page controls still work.
    }
  }

  function installStyles() {
    const css = `
      :root.xre-hide-left header[role="banner"] {
        visibility: hidden !important;
      }

      :root.xre-hide-right [data-testid="sidebarColumn"],
      :root.xre-hide-right aside[role="complementary"] {
        visibility: hidden !important;
      }

      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) [data-testid="tweetPhoto"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) [data-testid="videoPlayer"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) [data-testid="videoComponent"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) [data-testid="gifPlayer"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) [data-testid="playButton"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) a[href*="/photo/"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) a[href*="/video/"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) img[src*="pbs.twimg.com/media"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) img[src*="pbs.twimg.com/amplify_video_thumb"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) video {
        display: none !important;
      }

      .xre-media-toggle {
        display: none;
        width: 100%;
        min-height: 42px;
        margin: 10px 0 2px;
        padding: 10px 12px;
        border: 1px solid rgb(47, 51, 54);
        border-radius: 8px;
        background: rgba(29, 155, 240, 0.1);
        color: #1d9bf0;
        cursor: pointer;
        font: inherit;
        font-weight: 700;
        text-align: center;
      }

      .xre-media-toggle:hover,
      .xre-media-toggle:focus-visible {
        background: rgba(29, 155, 240, 0.18);
        outline: 2px solid rgba(29, 155, 240, 0.5);
        outline-offset: 2px;
      }

      :root.xre-hide-media article[data-xre-has-media="true"]:not([data-xre-media-expanded="true"]) .xre-media-toggle {
        display: block;
      }

      :root:not(.xre-hide-media) .xre-media-toggle {
        display: none !important;
      }

      article[data-xre-ad-hidden="true"] {
        display: none !important;
      }

      #xre-root {
        --xre-panel-offset: ${PANEL_OFFSET}px;
        position: fixed;
        right: 18px;
        bottom: 18px;
        width: 55px;
        height: 55px;
        z-index: 2147483647;
        color-scheme: dark;
        color: #eff3f4;
        scrollbar-color: rgb(62, 65, 68) rgb(22, 24, 28);
        font-family: TwitterChirp, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
        font-size: 14px;
        line-height: 1.35;
        -webkit-text-size-adjust: 100%;
        -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
      }

      #xre-root.xre-positioned {
        right: auto !important;
        bottom: auto !important;
      }

      #xre-root,
      #xre-root * {
        box-sizing: border-box;
      }

      #xre-root .xre-fab {
        display: flex;
        flex-basis: auto;
        flex-direction: column;
        flex-shrink: 0;
        align-self: flex-end;
        align-items: center;
        justify-content: center;
        width: 55px;
        height: 55px;
        min-width: 0;
        min-height: 0;
        margin: 0;
        padding: 0;
        border: 0 solid black;
        border-width: 1px;
        border-color: rgb(75, 78, 82);
        border-radius: 16px;
        background-color: rgba(0, 0, 0, 0.65);
        color: inherit;
        color-scheme: dark;
        box-shadow:
          rgba(255, 255, 255, 0.2) 0 0 15px,
          rgba(255, 255, 255, 0.15) 0 0 3px 1px;
        backdrop-filter: blur(12px);
        -webkit-backdrop-filter: blur(12px);
        box-sizing: border-box;
        cursor: pointer;
        font-family: inherit;
        font-size: inherit;
        font-weight: 800;
        list-style: none;
        outline-style: none;
        pointer-events: auto !important;
        text-align: inherit;
        text-decoration: none;
        transition-duration: 0.2s;
        transition-property: background-color, box-shadow;
        z-index: 0;
        touch-action: none;
      }

      #xre-root .xre-fab:hover,
      #xre-root .xre-fab:focus-visible {
        background-color: rgba(22, 24, 28, 0.75);
        box-shadow:
          rgba(255, 255, 255, 0.28) 0 0 18px,
          rgba(255, 255, 255, 0.18) 0 0 4px 1px;
      }

      #xre-root .xre-fab-icon {
        display: block;
        width: 27px;
        height: 27px;
        color: #eff3f4;
        pointer-events: none;
      }

      #xre-root .xre-visually-hidden {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        border: 0;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
      }

      #xre-root .xre-panel {
        position: absolute;
        right: var(--xre-panel-offset);
        bottom: var(--xre-panel-offset);
        width: 238px;
        padding: 14px;
        border: 1px solid rgba(239, 243, 244, 0.18);
        border-radius: 8px;
        background: rgba(0, 0, 0, 0.94);
        box-shadow: 0 10px 36px rgba(0, 0, 0, 0.45);
      }

      #xre-root.xre-panel-below .xre-panel {
        top: var(--xre-panel-offset);
        right: var(--xre-panel-offset);
        bottom: auto;
      }

      #xre-root.xre-panel-right .xre-panel {
        right: auto;
        left: var(--xre-panel-offset);
        bottom: var(--xre-panel-offset);
      }

      #xre-root.xre-panel-right.xre-panel-below .xre-panel {
        right: auto;
        left: var(--xre-panel-offset);
        top: var(--xre-panel-offset);
        bottom: auto;
      }

      #xre-root .xre-panel[hidden] {
        display: none !important;
      }

      #xre-root .xre-title {
        margin: 0 0 10px;
        color: #eff3f4;
        font-size: 15px;
        font-weight: 800;
        text-align: center;
        cursor: grab;
        user-select: none;
      }

      #xre-root.xre-dragging,
      #xre-root.xre-dragging * {
        cursor: grabbing !important;
        user-select: none !important;
      }

      #xre-root .xre-row {
        display: grid;
        grid-template-columns: 24px minmax(0, 1fr) 24px;
        align-items: center;
        min-height: 34px;
        gap: 12px;
        color: #eff3f4;
        cursor: pointer;
        user-select: none;
      }

      #xre-root .xre-row + .xre-row {
        border-top: 1px solid rgba(239, 243, 244, 0.12);
      }

      #xre-root .xre-row > span {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 2px;
        grid-column: 2;
        min-width: 0;
        text-align: center;
        overflow-wrap: anywhere;
      }

      #xre-root .xre-shortcut {
        color: #8b98a5;
        font-size: 11px;
        font-weight: 500;
        line-height: 1.1;
      }

      #xre-root .xre-row input {
        grid-column: 3;
        justify-self: end;
        width: 18px;
        height: 18px;
        margin: 0;
        accent-color: #1d9bf0;
      }

      @media (max-width: 720px) {
        #xre-root {
          right: 12px;
          bottom: 12px;
        }

        #xre-root .xre-panel {
          width: min(238px, calc(100vw - 24px));
        }
      }
    `;

    try {
      if (typeof GM_addStyle === "function") {
        GM_addStyle(css);
        return;
      }
    } catch (_) {
      // Fall through to a style element.
    }

    const style = document.createElement("style");
    style.id = "xre-style";
    style.textContent = css;
    (document.head || document.documentElement).appendChild(style);
  }

  function applyRootClasses() {
    const root = document.documentElement;
    root.classList.toggle("xre-hide-left", !settings.leftVisible);
    root.classList.toggle("xre-hide-right", !settings.rightVisible);
    root.classList.toggle("xre-hide-media", !settings.mediaVisible);
  }

  function onReady(callback) {
    if (document.body) {
      callback();
      return;
    }

    document.addEventListener("DOMContentLoaded", callback, { once: true });
  }

  function ensurePanel() {
    if (!document.body || document.getElementById("xre-root")) {
      return;
    }

    const root = document.createElement("div");
    root.id = "xre-root";
    root.innerHTML = `
      <button class="xre-fab" type="button" aria-label="X" aria-controls="xre-panel" aria-expanded="false" title="X">
        <svg class="xre-fab-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
          <path fill="currentColor" d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817-5.97 6.817H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"></path>
        </svg>
        <span class="xre-visually-hidden">X</span>
      </button>
      <section class="xre-panel" id="xre-panel" aria-label="X 阅读增强控制面板">
        <div class="xre-title">阅读控制</div>
        ${renderToggle("leftVisible", "显示左侧栏", "Shift+L")}
        ${renderToggle("rightVisible", "显示右侧栏", "Shift+R")}
        ${renderToggle("hideAds", "隐藏广告", "Shift+A")}
        ${renderToggle("mediaVisible", "默认显示媒体", "Shift+M")}
      </section>
    `;

    document.body.appendChild(root);

    const fab = root.querySelector(".xre-fab");
    const panel = root.querySelector(".xre-panel");
    const title = root.querySelector(".xre-title");
    let suppressFabClickUntil = 0;

    fab.addEventListener("click", (event) => {
      event.stopPropagation();

      if (Date.now() < suppressFabClickUntil) {
        return;
      }

      writeSetting("panelOpen", !settings.panelOpen);
      updatePanel(root);
    });

    root.addEventListener("change", (event) => {
      const input = event.target;

      if (!input || !input.matches || !input.matches("input[data-xre-key]")) {
        return;
      }

      writeSetting(input.dataset.xreKey, input.checked);
      applyRootClasses();
      scanPage();
      updatePanel(root);
    });

    document.addEventListener("keydown", (event) => {
      if (handleShortcut(event, root)) {
        return;
      }

      if (event.key !== "Escape" || panel.hidden) {
        return;
      }

      writeSetting("panelOpen", false);
      updatePanel(root);
      fab.focus();
    }, true);

    document.addEventListener("click", (event) => {
      if (panel.hidden || root.contains(event.target)) {
        return;
      }

      writeSetting("panelOpen", false);
      updatePanel(root);
    }, true);

    applyPanelPosition(root);
    setupPanelDrag(root, [fab, title], (handle) => {
      if (handle === fab) {
        suppressFabClickUntil = Date.now() + 250;
      }
    });
    updatePanel(root);
  }

  function renderToggle(key, label, shortcut) {
    return `
      <label class="xre-row" title="快捷键:${shortcut}">
        <span>
          <span>${label}</span>
          <span class="xre-shortcut">${shortcut}</span>
        </span>
        <input type="checkbox" data-xre-key="${key}">
      </label>
    `;
  }

  function handleShortcut(event, root) {
    if (
      !event.shiftKey ||
      event.ctrlKey ||
      event.altKey ||
      event.metaKey ||
      shouldIgnoreShortcutTarget(event.target)
    ) {
      return false;
    }

    const settingKey = SHORTCUTS[String(event.key || "").toUpperCase()];

    if (!settingKey) {
      return false;
    }

    event.preventDefault();
    event.stopPropagation();
    event.stopImmediatePropagation();
    writeSetting(settingKey, !settings[settingKey]);
    applyRootClasses();
    scanPage();
    updatePanel(root);
    return true;
  }

  function shouldIgnoreShortcutTarget(target) {
    if (!target || !target.closest) {
      return false;
    }

    return Boolean(
      target.closest(
        'input, textarea, select, [contenteditable="true"], [contenteditable=""], [role="textbox"]',
      ),
    );
  }

  function setupMediaToggleListener() {
    if (mediaToggleListenerInstalled) {
      return;
    }

    mediaToggleListenerInstalled = true;
    document.addEventListener("click", (event) => {
      const button = event.target?.closest?.(".xre-media-toggle");

      if (!button) {
        return;
      }

      const article = button.closest('article[data-testid="tweet"], article[role="article"]');

      if (!article) {
        return;
      }

      event.preventDefault();
      event.stopPropagation();
      event.stopImmediatePropagation();
      const expanded = article.dataset.xreMediaExpanded === "true";

      if (expanded) {
        delete article.dataset.xreMediaExpanded;
      } else {
        article.dataset.xreMediaExpanded = "true";
      }

      updateMediaToggleText(article);
    }, true);
  }

  function updatePanel(root) {
    const fab = root.querySelector(".xre-fab");
    const panel = root.querySelector(".xre-panel");

    if (fab && panel) {
      panel.hidden = !settings.panelOpen;
      fab.setAttribute("aria-expanded", String(settings.panelOpen));
    }

    root.querySelectorAll("input[data-xre-key]").forEach((input) => {
      input.checked = Boolean(settings[input.dataset.xreKey]);
    });

    updatePanelPlacement(root);
  }

  function setupPanelDrag(root, handles, onDragEnd) {
    handles.filter(Boolean).forEach((handle) => {
      handle.addEventListener("pointerdown", (event) => {
        if (event.button !== 0) {
          return;
        }

        const startRect = root.getBoundingClientRect();
        const state = {
          handle,
          pointerId: event.pointerId,
          startX: event.clientX,
          startY: event.clientY,
          startLeft: startRect.left,
          startTop: startRect.top,
          moved: false,
        };

        const onPointerMove = (moveEvent) => {
          if (moveEvent.pointerId !== state.pointerId) {
            return;
          }

          const dx = moveEvent.clientX - state.startX;
          const dy = moveEvent.clientY - state.startY;

          if (Math.hypot(dx, dy) > DRAG_CLICK_TOLERANCE) {
            state.moved = true;
          }

          setPanelPosition(root, clampPanelPosition(root, state.startLeft + dx, state.startTop + dy));
        };

        const onPointerUp = (upEvent) => {
          if (upEvent.pointerId !== state.pointerId) {
            return;
          }

          document.removeEventListener("pointermove", onPointerMove);
          document.removeEventListener("pointerup", onPointerUp);
          document.removeEventListener("pointercancel", onPointerUp);
          root.classList.remove("xre-dragging");

          if (state.moved) {
            const rect = root.getBoundingClientRect();
            const position = clampPanelPosition(root, rect.left, rect.top);
            setPanelPosition(root, position);
            writeSetting("panelPosition", position);
            onDragEnd(state.handle);
          }
        };

        event.preventDefault();
        root.classList.add("xre-dragging");
        handle.setPointerCapture?.(event.pointerId);
        document.addEventListener("pointermove", onPointerMove);
        document.addEventListener("pointerup", onPointerUp);
        document.addEventListener("pointercancel", onPointerUp);
      });
    });

    window.addEventListener("resize", () => {
      const rootElement = document.getElementById("xre-root");

      if (!rootElement) {
        return;
      }

      if (isValidPosition(settings.panelPosition)) {
        const position = clampPanelPosition(rootElement, settings.panelPosition.left, settings.panelPosition.top);
        setPanelPosition(rootElement, position);
        writeSetting("panelPosition", position);
      }

      updatePanelPlacement(rootElement);
    });
  }

  function applyPanelPosition(root) {
    if (!isValidPosition(settings.panelPosition)) {
      updatePanelPlacement(root);
      return;
    }

    setPanelPosition(root, clampPanelPosition(root, settings.panelPosition.left, settings.panelPosition.top));
  }

  function isValidPosition(position) {
    return (
      position &&
      Number.isFinite(position.left) &&
      Number.isFinite(position.top)
    );
  }

  function clampPanelPosition(root, left, top) {
    const rect = root.getBoundingClientRect();
    const width = rect.width || 55;
    const height = rect.height || 55;
    const maxLeft = Math.max(DRAG_MARGIN, window.innerWidth - width - DRAG_MARGIN);
    const maxTop = Math.max(DRAG_MARGIN, window.innerHeight - height - DRAG_MARGIN);

    return {
      left: Math.round(Math.min(Math.max(DRAG_MARGIN, left), maxLeft)),
      top: Math.round(Math.min(Math.max(DRAG_MARGIN, top), maxTop)),
    };
  }

  function setPanelPosition(root, position) {
    root.classList.add("xre-positioned");
    root.style.left = `${position.left}px`;
    root.style.top = `${position.top}px`;
    root.style.right = "auto";
    root.style.bottom = "auto";
    updatePanelPlacement(root);
  }

  function updatePanelPlacement(root) {
    const panel = root.querySelector(".xre-panel");

    if (!panel) {
      return;
    }

    const rootRect = root.getBoundingClientRect();
    const panelWidth = panel.offsetWidth || 238;
    const panelHeight = panel.offsetHeight || 210;
    const needsRightExpand = rootRect.right < panelWidth + PANEL_OFFSET + DRAG_MARGIN;
    const needsBelow = rootRect.top < panelHeight + PANEL_OFFSET + DRAG_MARGIN;

    root.classList.toggle("xre-panel-right", needsRightExpand);
    root.classList.toggle("xre-panel-below", needsBelow);
  }

  function startObserver() {
    if (observer || !document.body) {
      return;
    }

    observer = new MutationObserver(queueScan);
    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
  }

  function queueScan() {
    if (scanQueued) {
      return;
    }

    scanQueued = true;
    window.requestAnimationFrame(() => {
      scanQueued = false;
      ensurePanel();
      scanPage();
    });
  }

  function scanPage() {
    applyRootClasses();

    const articles = document.querySelectorAll('article[data-testid="tweet"], article[role="article"]');
    articles.forEach((article) => {
      syncArticleMediaToggle(article);

      if (settings.hideAds && isPromotedArticle(article)) {
        article.dataset.xreAdHidden = "true";
      } else {
        delete article.dataset.xreAdHidden;
      }
    });
  }

  function syncArticleMediaToggle(article) {
    const hasMedia = Boolean(article.querySelector(MEDIA_SELECTORS));
    const button = article.querySelector(".xre-media-toggle");

    if (!hasMedia) {
      delete article.dataset.xreHasMedia;
      delete article.dataset.xreMediaExpanded;
      button?.remove();
      return;
    }

    article.dataset.xreHasMedia = "true";

    if (!button) {
      insertMediaToggle(article);
    } else {
      updateMediaToggleText(article);
    }
  }

  function insertMediaToggle(article) {
    const button = document.createElement("button");
    button.className = "xre-media-toggle";
    button.type = "button";
    updateMediaToggleText(article, button);

    const mediaBlock = findMediaBlock(article);

    if (mediaBlock?.parentElement) {
      mediaBlock.parentElement.insertBefore(button, mediaBlock);
      return;
    }

    article.appendChild(button);
  }

  function findMediaBlock(article) {
    const block = article.querySelector(MEDIA_BLOCK_SELECTORS);

    if (block) {
      return block;
    }

    const media = article.querySelector(MEDIA_SELECTORS);
    return media?.closest?.(MEDIA_BLOCK_SELECTORS) || media;
  }

  function updateMediaToggleText(article, button = article.querySelector(".xre-media-toggle")) {
    if (!button) {
      return;
    }

    button.textContent = article.dataset.xreMediaExpanded === "true"
      ? "隐藏图片/视频"
      : "显示图片/视频";
  }

  function isPromotedArticle(article) {
    if (article.querySelector('[data-testid="placementTracking"]')) {
      return true;
    }

    const labelNodes = article.querySelectorAll('span, div[dir="auto"], [aria-label]');

    for (const node of labelNodes) {
      if (node.closest('[data-testid="tweetText"]')) {
        continue;
      }

      if (isPromotedText(node.textContent) || isPromotedText(node.getAttribute("aria-label"))) {
        return true;
      }
    }

    return false;
  }

  function isPromotedText(value) {
    const text = normalizeText(value);

    if (!text || text.length > 64) {
      return false;
    }

    return AD_PATTERNS.some((pattern) => pattern.test(text));
  }

  function normalizeText(value) {
    return String(value || "")
      .replace(/\s+/g, " ")
      .replace(/[\u00a0\u200b-\u200d\ufeff]/g, "")
      .trim();
  }
})();