Greasy Fork is available in English.
Efficiently hides reposts on X with optional keyword filtering
// ==UserScript==
// @name Smarter Repost Hider for X
// @namespace https://github.com/xechostormx/repost-hider
// @version 2.1.1
// @description Efficiently hides reposts on X with optional keyword filtering
// @match https://x.com/*
// @match https://www.x.com/*
// @match https://mobile.x.com/*
// @run-at document-end
// @grant none
// @license MIT
// ==/UserScript==
(() => {
'use strict';
// ———————— CONFIG ————————
const config = {
debug: false, // true → console.debug logs
keywordFilter: [] // e.g. ['spam','ad'] to only hide reposts containing those
};
const sel = {
tweet: '[data-testid="cellInnerDiv"]',
repost: '[data-testid="socialContext"]',
tweetTxt: '[data-testid="tweetText"]'
};
let hiddenCount = 0;
const log = (...args) => config.debug && console.debug('[RepostHider]', ...args);
// ———————— CORE LOGIC ————————
function isRepost(el) {
return !!el.querySelector(sel.repost);
}
function matchesKeywords(el) {
if (!config.keywordFilter.length) return true;
const txtEl = el.querySelector(sel.tweetTxt);
const text = txtEl?.textContent.toLowerCase() || '';
return config.keywordFilter.some(k => text.includes(k.toLowerCase()));
}
function processTweet(el) {
// skip if already seen
if (el.dataset.repostHandled) return;
el.dataset.repostHandled = '1';
if (isRepost(el) && matchesKeywords(el)) {
el.style.display = 'none';
hiddenCount++;
log(`Hid repost #${hiddenCount}`, el);
}
}
function scanRoot(root = document.body) {
root.querySelectorAll(sel.tweet).forEach(processTweet);
}
// ———————— DYNAMIC OBSERVER ————————
const obs = new MutationObserver(muts => {
for (const m of muts) {
for (const node of m.addedNodes) {
if (node.nodeType !== 1) continue;
// if the node itself is a tweet
if (node.matches && node.matches(sel.tweet)) {
processTweet(node);
} else {
// or contains tweets
node.querySelectorAll?.(sel.tweet).forEach(processTweet);
}
}
}
});
function initObserver() {
obs.observe(document.body, { childList: true, subtree: true });
}
// ———————— SPA NAV HANDLER ————————
function watchUrlChange() {
let last = location.href;
new MutationObserver(() => {
if (location.href !== last) {
last = location.href;
log('URL changed, rescanning feed');
scanRoot();
}
}).observe(document, { childList: true, subtree: true });
}
// ———————— BOOTSTRAP ————————
function init() {
scanRoot();
initObserver();
watchUrlChange();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();