Hide search results by subreddit or user
// ==UserScript==
// @name Reddit Search - Hide subs/users
// @namespace Reddit Search - Hide subs/users
// @version 1.0
// @description Hide search results by subreddit or user
// @match https://www.reddit.com/search/*
// @icon https://redditinc.com/hs-fs/hubfs/Reddit%20Inc/Content/Brand%20Page/Reddit_Logo.png?width=200&height=200&name=Reddit_Logo.png
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const SUBS_KEY = 'rs_hidden_subs';
const USERS_KEY = 'rs_hidden_users';
const hiddenSubs = new Set((GM_getValue(SUBS_KEY, []) || []).map(x => String(x).toLowerCase()));
const hiddenUsers = new Set((GM_getValue(USERS_KEY, []) || []).map(x => String(x).toLowerCase()));
GM_addStyle(`
.rs-x {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
margin-left: 6px;
border: 1px solid rgba(255,255,255,.18);
border-radius: 999px;
background: rgba(255,255,255,.06);
color: #ff6b6b;
font: 700 12px/1 sans-serif;
cursor: pointer;
user-select: none;
vertical-align: middle;
padding: 0;
position: relative;
top: -1px;
}
.rs-x:hover { background: rgba(255,255,255,.12); }
.rs-hidden { display: none !important; }
.rs-toast-wrap {
position: fixed;
right: 16px;
bottom: 16px;
z-index: 2147483647;
display: flex;
flex-direction: column;
gap: 8px;
pointer-events: none;
}
.rs-toast {
display: flex;
align-items: center;
gap: 10px;
max-width: 420px;
padding: 10px 12px;
border-radius: 10px;
background: #1f1f1f;
color: #fff;
border: 1px solid rgba(255,255,255,.12);
box-shadow: 0 10px 30px rgba(0,0,0,.35);
pointer-events: auto;
}
.rs-undo {
border: 0;
border-radius: 999px;
padding: 5px 10px;
background: #ff4500;
color: #fff;
font-weight: 700;
cursor: pointer;
}
`);
function normSub(s) {
return String(s || '').trim().replace(/^\/?r\//i, '').toLowerCase();
}
function normUser(s) {
return String(s || '').trim().replace(/^\/?u\//i, '').toLowerCase();
}
function save() {
GM_setValue(SUBS_KEY, [...hiddenSubs]);
GM_setValue(USERS_KEY, [...hiddenUsers]);
}
function stop(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
function parseCtx(el) {
try {
return JSON.parse(el.getAttribute('data-faceplate-tracking-context') || '{}');
} catch {
return {};
}
}
function toast(msg, undoFn) {
let wrap = document.querySelector('.rs-toast-wrap');
if (!wrap) {
wrap = document.createElement('div');
wrap.className = 'rs-toast-wrap';
document.body.appendChild(wrap);
}
const t = document.createElement('div');
t.className = 'rs-toast';
const span = document.createElement('span');
span.textContent = msg;
const btn = document.createElement('button');
btn.className = 'rs-undo';
btn.textContent = 'Undo';
btn.addEventListener('click', (e) => {
stop(e);
undoFn();
t.remove();
}, true);
t.append(span, btn);
wrap.append(t);
setTimeout(() => t.remove(), 5000);
}
function makeX(onClick) {
const b = document.createElement('button');
b.className = 'rs-x';
b.type = 'button';
b.textContent = '×';
b.addEventListener('mousedown', stop, true);
b.addEventListener('click', (e) => {
stop(e);
onClick();
}, true);
return b;
}
function applyHides(root = document) {
root.querySelectorAll('search-telemetry-tracker[view-events="search/view/post"]').forEach(tracker => {
const sub = normSub(parseCtx(tracker).subreddit?.name);
tracker.classList.toggle('rs-hidden', !!sub && hiddenSubs.has(sub));
});
root.querySelectorAll('search-telemetry-tracker[view-events="search/view/people"]').forEach(tracker => {
const user = normUser(parseCtx(tracker).profile?.name);
tracker.classList.toggle('rs-hidden', !!user && hiddenUsers.has(user));
});
}
function addPostButtons(root = document) {
root.querySelectorAll('search-telemetry-tracker[view-events="search/view/post"]').forEach(tracker => {
if (!(tracker instanceof HTMLElement)) return;
const sub = normSub(parseCtx(tracker).subreddit?.name);
if (!sub) return;
const timeEl =
tracker.querySelector('faceplate-timeago time') ||
tracker.querySelector('time');
if (!timeEl) return;
if (timeEl.parentElement?.querySelector(':scope > .rs-x')) return;
const btn = makeX(() => {
hiddenSubs.add(sub);
save();
applyHides(document);
toast(`Hidden r/${sub}`, () => {
hiddenSubs.delete(sub);
save();
applyHides(document);
});
});
timeEl.insertAdjacentElement('afterend', btn);
});
}
function addPeopleButtons(root = document) {
root.querySelectorAll('search-telemetry-tracker[view-events="search/view/people"]').forEach(tracker => {
if (!(tracker instanceof HTMLElement)) return;
const user = normUser(parseCtx(tracker).profile?.name);
if (!user) return;
const nameEl =
tracker.querySelector('[data-testid="search-author"] h3 span[id^="search-results-people-"]') ||
tracker.querySelector('[data-testid="search-author"] h3 span');
if (!nameEl) return;
if (nameEl.parentElement?.querySelector(':scope > .rs-x')) return;
const btn = makeX(() => {
hiddenUsers.add(user);
save();
applyHides(document);
toast(`Hidden u/${user}`, () => {
hiddenUsers.delete(user);
save();
applyHides(document);
});
});
nameEl.insertAdjacentElement('afterend', btn);
});
}
function refresh(root = document) {
addPostButtons(root);
addPeopleButtons(root);
applyHides(root);
}
let bootTimer = null;
function bootRefreshBurst() {
if (bootTimer) clearInterval(bootTimer);
let runs = 0;
bootTimer = setInterval(() => {
refresh(document);
runs++;
if (runs >= 40) {
clearInterval(bootTimer);
bootTimer = null;
}
}, 250);
}
const mo = new MutationObserver(muts => {
let needs = false;
for (const m of muts) {
for (const node of m.addedNodes) {
if (!(node instanceof HTMLElement)) continue;
needs = true;
refresh(node);
}
}
if (needs) refresh(document);
});
function hookNavigation() {
const _pushState = history.pushState;
const _replaceState = history.replaceState;
history.pushState = function () {
const r = _pushState.apply(this, arguments);
setTimeout(() => {
refresh(document);
bootRefreshBurst();
}, 50);
return r;
};
history.replaceState = function () {
const r = _replaceState.apply(this, arguments);
setTimeout(() => {
refresh(document);
bootRefreshBurst();
}, 50);
return r;
};
window.addEventListener('popstate', () => {
setTimeout(() => {
refresh(document);
bootRefreshBurst();
}, 50);
});
}
function init() {
refresh(document);
bootRefreshBurst();
mo.observe(document.documentElement, { childList: true, subtree: true });
hookNavigation();
window.addEventListener('load', () => {
refresh(document);
bootRefreshBurst();
});
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
refresh(document);
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();