X(Twitter) のタイムラインで長文ツイートだけを隠す Chrome 拡張の作り方。下のファイルをコピペすれば同じように動く。

できること

  • 140文字超 or「もっと見る」が付くツイートを非表示にする
  • ポップアップで ON/OFF を切り替え(設定は保存される)
  • スクロールで流れてくる新規ツイートにも自動で効く

用意するファイル

拡張用フォルダに次の4ファイル(と任意で icon.png)を置く。

  • manifest.json … 拡張の設定
  • content.js … タイムラインに注入するスクリプト(長文判定・非表示)
  • popup.html … ポップアップのHTML
  • popup.js … ポップアップのON/OFF処理
  • icon.png … アイコン(256×256推奨。任意)

manifest.json(全文)

{
  "name": "X長文フィルタ",
  "manifest_version": 3,
  "version": "1.0",
  "permissions": ["activeTab", "storage"],
  "content_scripts": [
    {
      "matches": ["https://twitter.com/*", "https://x.com/*"],
      "js": ["content.js"]
    }
  ],
  "action": {
    "default_popup": "popup.html"
  },
  "icons": {
    "256": "icon.png"
  }
}

icon.png を使わないなら "icons" ブロックごと消してよい。

content.js(全文)

(function() {
    'use strict';

    const MAX_LENGTH = 140;
    const SHOW_MORE_SELECTOR = '[data-testid="tweet-text-show-more-link"]';
    let isFilterEnabled = true;

    chrome.storage.local.get(['filterEnabled'], (result) => {
        isFilterEnabled = result.filterEnabled !== false;
        applyVisibilityAll();
    });

    chrome.storage.onChanged.addListener((changes) => {
        if (changes.filterEnabled) {
            isFilterEnabled = changes.filterEnabled.newValue;
            applyVisibilityAll();
        }
    });

    function processTweets() {
        const articles = document.querySelectorAll('article[data-testid="tweet"]');
        articles.forEach(article => {
            if (article.dataset.isLongChecked) {
                updateArticleVisibility(article);
                return;
            }
            const textNode = article.querySelector('div[data-testid="tweetText"]');
            const showMoreBtn = article.querySelector(SHOW_MORE_SELECTOR);
            let isLong = false;
            if (textNode) {
                const textContent = textNode.textContent || "";
                if (textContent.length > MAX_LENGTH || showMoreBtn !== null) isLong = true;
            }
            article.dataset.isLongTweet = isLong;
            article.dataset.isLongChecked = "true";
            updateArticleVisibility(article);
        });
    }

    function updateArticleVisibility(article) {
        const isLong = article.dataset.isLongTweet === "true";
        if (isFilterEnabled && isLong) {
            if (article.style.display !== 'none') article.style.display = 'none';
        } else {
            if (article.style.display === 'none') article.style.display = '';
        }
    }

    function applyVisibilityAll() {
        const articles = document.querySelectorAll('article[data-testid="tweet"]');
        articles.forEach(article => updateArticleVisibility(article));
    }

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

popup.html(全文)

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    body { width: 200px; padding: 10px; font-family: sans-serif; }
    .container { display: flex; align-items: center; justify-content: space-between; }
    .label { font-size: 14px; font-weight: bold; }
    .switch { position: relative; display: inline-block; width: 40px; height: 24px; }
    .switch input { opacity: 0; width: 0; height: 0; }
    .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 24px; }
    .slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; }
    input:checked + .slider { background-color: #1DA1F2; }
    input:checked + .slider:before { transform: translateX(16px); }
  </style>
</head>
<body>
  <div class="container">
    <span class="label">長文非表示</span>
    <label class="switch">
      <input type="checkbox" id="toggleFilter">
      <span class="slider"></span>
    </label>
  </div>
  <script src="popup.js"></script>
</body>
</html>

popup.js(全文)

document.addEventListener('DOMContentLoaded', () => {
    const toggle = document.getElementById('toggleFilter');
    chrome.storage.local.get(['filterEnabled'], (result) => {
        toggle.checked = result.filterEnabled !== false;
    });
    toggle.addEventListener('change', () => {
        chrome.storage.local.set({ filterEnabled: toggle.checked });
    });
});

入れ方

  1. 4ファイル(と必要なら icon.png)を同じフォルダに保存する
  2. Chrome で chrome://extensions/ を開く
  3. 「デベロッパーモード」を ON にする
  4. 「パッケージ化されていない拡張機能を読み込む」でそのフォルダを指定する

X のタブで長文が非表示になり、拡張アイコンから ON/OFF を切り替えられる。

補足

  • 長文の基準(140文字)は content.jsMAX_LENGTH を変えればよい。
  • X の DOM(data-testid など)が変わると動かなくなることがある。そのときはセレクタを直す。