X のタイムラインを流し見していると、考察や連投をまとめた長文ツイートで急に密度が上がって、 ざっと眺めたいときには逆に読みにくくなる。短い投稿だけを拾い読みしたいときに、 長文ツイートをまとめて隠せると都合がいい。
ここではタイムライン上の長文ツイートを自動で非表示にする Chrome 拡張を自作する。 ツールバーのアイコンから ON/OFF を切り替えられて、設定はブラウザに保存されるので、 次に開いたときも前回の状態が続く。下の4ファイルをそのまま保存すれば同じように動く。
できること
- 140文字超 or「もっと見る」が付くツイートを非表示にする
- ポップアップで ON/OFF を切り替え(設定は保存される)
- スクロールで流れてくる新規ツイートにも自動で効く
用意するファイル
拡張用フォルダに次の4ファイル(と任意で icon.png)を置く。
manifest.json… 拡張の設定content.js… タイムラインに注入するスクリプト(長文判定・非表示)popup.html… ポップアップのHTMLpopup.js… ポップアップのON/OFF処理icon.png… アイコン(256×256推奨。任意)
仕組みの全体像
中心になるのは content.js で、これが X のページに読み込まれてタイムラインを監視する。
ツイート1件は article 要素になっていて、その本文の文字数が基準を超えるか、
「もっと見る」が付いているものを長文と見なし、display:none で行ごと隠している。
タイムラインはスクロールのたびに新しいツイートが後から差し込まれるので、一度きりの処理では追いつかない。
そこで MutationObserver でページの変化を監視して、ツイートが増えるたびに判定を走らせている。
ON/OFF の状態は chrome.storage に保存し、ポップアップで切り替えると content.js が
その変化を受け取って、開いているタブの表示へすぐ反映する。
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: var(--color-border-strong); transition: .4s; border-radius: 24px; }
.slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 4px; bottom: 4px; background-color: var(--color-on-accent); 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 });
});
}); 入れ方
- 4ファイル(と必要なら icon.png)を同じフォルダに保存する
- Chrome で
chrome://extensions/を開く - 「デベロッパーモード」を ON にする
- 「パッケージ化されていない拡張機能を読み込む」でそのフォルダを指定する
X のタブで長文が非表示になり、拡張アイコンから ON/OFF を切り替えられる。
カスタマイズ
- 長文と見なす文字数は
content.jsのMAX_LENGTHで決めている。140 を増やせば、より長いものだけが対象になる。 - 「もっと見る」が付くツイートも対象に含めているのは、X が一定の長さで本文を折りたたむため。折りたたみの有無だけで判定したいなら、文字数の条件を外して
showMoreBtnの有無だけにすればいい。
つまずきやすいところ・注意
- X 側の DOM は予告なく変わる。急に効かなくなったら、
article[data-testid="tweet"]やtweetTextのセレクタが今も合っているかを確認して直す。 - 判定済みのツイートは
datasetに結果を残して使い回している。挙動が変なときはタブをリロードすると、最初から判定し直せる。 - これは自分のブラウザ上での見え方を変えるだけで、相手の投稿や通知には何も影響しない。手元のタイムラインが読みやすくなるだけのもの。