chatGPTに作らせたTampermonkeyスクリプト
仕様は以下
・それぞれのユーザーホーム画面で機能する
・RTはデフォルト隠す
・RT隠す/表示するボタンを用意する
・RT個別に表示することも可能
使ってみた画面
コード
chatGPTの出力を実際のDOMに合わせて修正済み
// ==UserScript==
// @name X/Twitter Profile Repost Hider (Japanese UI, robust)
// @namespace https://example.local/
// @version 1.1
// @description ユーザープロフィール(/username)で再投稿(RT/Repost)をデフォルト非表示。ページ上のトグル(日本語)と個別表示ボタンあり。Tampermonkey用。
// @match https://twitter.com/*
// @match https://x.com/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function(){
'use strict';
const HIDDEN_CLASS = 'rthider_hidden_by_script_v11';
const PER_ITEM_SHOWN = 'rthider_shown_item_v11';
const UI_ID = 'rthider_ui_v11';
const PROCESSED_FLAG = 'rthider_processed_v11';
// 判定用正規表現(英語・日本語・一般的な語句を網羅)
const REPOST_RE = /(retweet|retweeted|repost|reposted|reposted by|repostado|リツイート|再投稿|再ポスト|転送|リポスト|reposted|repostado)/i;
// profile page 判定(/username のみ)
function isProfileRoot() {
const p = location.pathname.split('/').filter(Boolean);
if (p.length !== 1) return false;
const single = p[0].toLowerCase();
if (['home','explore','i','notifications','search','messages','settings','compose','search'].includes(single)) return false;
return true;
}
// tweet ノード候補を見つける(article, [data-testid="tweet"], role="article")
function findTweetNodes(root=document) {
const nodes = new Set();
root.querySelectorAll('article, [data-testid="tweet"], [role="article"]').forEach(n=>nodes.add(n));
return Array.from(nodes);
}
// テキストを安全に取得
function textOf(el) {
try { return (el && (el.innerText || el.textContent || '')).trim(); } catch(e) { return ''; }
}
// RT / 再投稿かどうか判定
function isRepostNode(node) {
if (!node) return false;
// 1) socialContext 系要素
const sc = node.querySelector('[data-testid="socialContext"], [data-testid="social-context"], [data-testid^="social"]');
if (sc && REPOST_RE.test(textOf(sc))) {
return true;
}
// // 2) aria-label に該当語があるか
// const withAria = node.querySelectorAll('[aria-label]');
// for (const a of withAria) {
// if (REPOST_RE.test(a.getAttribute('aria-label') || '')) return true;
// }
// // 3) 文字列検索(直近の文言に "Reposted by" や "Retweeted by" が出るケース)
// if (REPOST_RE.test(textOf(node))) return true;
// // 4) 追加チェック: 親要素に socialContext などがあるか
// const parentSc = node.closest('[data-testid="socialContext"], [data-testid="social-context"]');
// if (parentSc && REPOST_RE.test(textOf(parentSc))) return true;
// mark as checked (avoid reprocessing)
return false;
}
// 個別非表示処理(プレースホルダ挿入)
function hideTweet(node) {
try {
if (!node || node.classList.contains(HIDDEN_CLASS) || node.classList.contains(PER_ITEM_SHOWN)) return;
// マークしておく
node.dataset[PROCESSED_FLAG] = '1';
// placeholder
const ph = document.createElement('div');
ph.className = 'rthider_placeholder';
ph.style.cssText = 'padding:8px;border-radius:8px;margin:6px 0;border-left:4px solid #d0d0d0;background:rgba(0,0,0,0.03);font-size:13px;';
const label = document.createElement('span');
label.innerText = 'この再投稿を非表示にしています';
ph.appendChild(label);
const btn = document.createElement('button');
btn.innerText = 'この再投稿を表示';
btn.style.cssText = 'margin-left:10px;padding:4px 8px;border-radius:6px;cursor:pointer';
btn.onclick = (e) => {
e.stopPropagation();
// placeholder を削除して tweet を表示
if (ph.parentNode) ph.parentNode.removeChild(ph);
node.classList.remove(HIDDEN_CLASS);
node.classList.add(PER_ITEM_SHOWN);
};
ph.appendChild(btn);
// insert placeholder immediately before node and hide node
node.parentNode && node.parentNode.insertBefore(ph, node);
node.classList.add(HIDDEN_CLASS);
} catch(e) {
console.warn('rthider hide error', e);
}
}
function showAll() {
document.querySelectorAll('.' + HIDDEN_CLASS).forEach(n=>{
// remove placeholder if present
const prev = n.previousSibling;
if (prev && prev.classList && prev.classList.contains('rthider_placeholder')) prev.remove();
n.classList.remove(HIDDEN_CLASS);
});
}
function hideAll() {
// scan existing nodes
findTweetNodes(document).forEach(n=>{
if (!n) return;
if (n.classList && (n.classList.contains(PER_ITEM_SHOWN))) return;
if (isRepostNode(n)) hideTweet(n);
// mark processed so we don't keep re-evaluating heavy nodes
try{ n.dataset[PROCESSED_FLAG] = '1'; }catch(e){}
});
}
// UI(画面右上に固定で日本語のトグルを表示)
function insertFloatingUI() {
if (!isProfileRoot()) return;
if (document.getElementById(UI_ID)) return;
const ui = document.createElement('div');
ui.id = UI_ID;
ui.style.cssText = 'position:fixed;top:0px;right:18px;z-index:2147483647;background:#fff;border:1px solid #ddd;padding:8px;border-radius:10px;box-shadow:0 6px 18px rgba(0,0,0,0.12);font-family:system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;';
ui.setAttribute('aria-hidden','false');
const btn = document.createElement('button');
btn.id = UI_ID + '_toggle';
btn.innerText = 'RTを表示';
btn.style.cssText = 'padding:6px 8px;border-radius:8px;border:1px solid #ccc;cursor:pointer;font-size:13px;';
btn.dataset.hidden = 'true';
btn.onclick = () => {
const hidden = btn.dataset.hidden === 'true';
if (hidden) {
// 切り替え -> 表示
showAll();
btn.dataset.hidden = 'false';
btn.innerText = 'RTを非表示';
} else {
// 切り替え -> 非表示
hideAll();
btn.dataset.hidden = 'true';
btn.innerText = 'RTを表示';
}
};
const resc = document.createElement('button');
resc.innerText = '再スキャン';
resc.style.cssText = 'margin-left:8px;padding:6px 8px;border-radius:8px;border:1px solid #ccc;cursor:pointer;font-size:13px;';
resc.onclick = () => { hideAll(); };
ui.appendChild(btn);
ui.appendChild(resc);
document.body.appendChild(ui);
}
// MutationObserver: 動的に増えるツイートを監視して処理する
const mo = new MutationObserver((muts) => {
if (!isProfileRoot()) return;
insertFloatingUI();
const btn = document.getElementById(UI_ID + '_toggle');
if (btn.dataset.hidden === 'false') return;
for (const m of muts) {
for (const n of m.addedNodes) {
if (!(n instanceof Element)) continue;
// check nodes themselves and their descendants for tweets
const candidates = findTweetNodes(n);
if (candidates.length === 0) {
// sometimes the direct added node *is* the tweet
if (n.matches && (n.matches('article') || n.matches('[data-testid="tweet"]') || n.matches('[role="article"]'))) {
candidates.push(n);
}
}
for (const c of candidates) {
// skip if already explicitly shown by user
if (c.classList && c.classList.contains(PER_ITEM_SHOWN)) continue;
// skip if already processed
if (c.dataset && c.dataset[PROCESSED_FLAG]) continue;
if (isRepostNode(c)) hideTweet(c);
// mark processed
try{ c.dataset[PROCESSED_FLAG] = '1'; }catch(e){}
}
}
}
});
mo.observe(document.body, { childList: true, subtree: true });
// CSS
const style = document.createElement('style');
style.textContent = `
.${HIDDEN_CLASS} { display: none !important; }
.${PER_ITEM_SHOWN} { display: block !important; }
.rthider_placeholder { transition: opacity .18s ease; }
`;
document.head.appendChild(style);
// 初回実行(少し遅延してページの初期レンダリングを待つ)
setTimeout(()=>{
if (isProfileRoot()) {
insertFloatingUI();
hideAll();
}
}, 900);
// デバッグ用: コンソールから手動トリガー可能
window.rthider = {
hideAll: hideAll,
showAll: showAll,
scanNow: () => { hideAll(); console.log('rthider: scan done'); }
};
})();

コメント