Adds a persistent Hide button to Reddit posts and search results, and lets you press h to hide the post under the cursor.
// ==UserScript==
// @name Reddit - Hide Posts
// @namespace Reddit-hide-posts
// @version 1.9.1
// @description Adds a persistent Hide button to Reddit posts and search results, and lets you press h to hide the post under the cursor.
// @match https://*.reddit.com/*
// @icon https://redditinc.com/hs-fs/hubfs/Reddit%20Inc/Content/Brand%20Page/Reddit_Logo.png?width=200&height=200&name=Reddit_Logo.png
// @run-at document-idle
// @grant none
// @license MIT
// ==/UserScript==
(() => {
"use strict";
// ---------------------------------------------------------------------------
// Selectors
// ---------------------------------------------------------------------------
const POST_SELECTOR = "shreddit-post[id]";
const SEARCH_POST_SELECTOR = 'search-telemetry-tracker[data-testid="search-sdui-post"][data-thingid]';
const ALL_POST_SELECTOR = `${POST_SELECTOR}, ${SEARCH_POST_SELECTOR}`;
const ACTION_ROW_SELECTOR = '[data-testid="action-row"]';
const OVERFLOW_MENU_SELECTOR = "shreddit-post-overflow-menu";
const SEARCH_POST_UNIT_SELECTOR = '[data-testid="search-post-unit"]';
const SEARCH_POST_CONTENT_SELECTOR = '[data-testid="sdui-post-unit"]';
const HIDE_BUTTON_ATTR = "data-codex-hide-button";
const TOP_BUTTONS_ATTR = "data-codex-top-buttons";
const BUTTON_CLASS_NAME = "button border-md flex flex-row justify-center items-center h-xl font-semibold relative text-caption-1 button-secondary inline-flex px-sm";
const hoveredState = { post: null };
let scheduledEnhance = false;
// ---------------------------------------------------------------------------
// Core Post Helpers
// ---------------------------------------------------------------------------
function isSearchResultPost(post) {
return post instanceof HTMLElement && post.matches(SEARCH_POST_SELECTOR);
}
function isEditableTarget(target) {
if (!(target instanceof Element)) return false;
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) return true;
if (target.isContentEditable) return true;
return Boolean(target.closest('[contenteditable=""],[contenteditable="true"]'));
}
function setHoveredPost(post) {
if (hoveredState.post !== post) hoveredState.post = post || null;
}
function findPostFromEvent(event) {
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
for (const item of path) {
if (item instanceof HTMLElement && item.matches?.(POST_SELECTOR)) return item;
if (item instanceof HTMLElement && item.matches?.(SEARCH_POST_SELECTOR)) return item;
}
const target = event.target;
return target instanceof Element ? target.closest(ALL_POST_SELECTOR) : null;
}
// ---------------------------------------------------------------------------
// Native Hide
// ---------------------------------------------------------------------------
function textHasHide(el) {
return /\bhide\b/i.test(el.textContent);
}
function scanRootForHide(root) {
if (!root) return null;
for (const el of root.querySelectorAll('[role="menuitem"]')) {
if (textHasHide(el)) return el;
}
for (const li of root.querySelectorAll("faceplate-menu li, li[rpl]")) {
if (textHasHide(li)) {
return li.querySelector("button, a") ?? li;
}
}
for (const el of root.querySelectorAll("button, a")) {
if (/^\s*hide\s*$/i.test(el.textContent)) return el;
}
return null;
}
function findHideMenuItem(overflowMenu) {
const fromOwn = scanRootForHide(overflowMenu.shadowRoot) ?? scanRootForHide(overflowMenu);
if (fromOwn) return fromOwn;
for (const dd of document.querySelectorAll("faceplate-dropdown-menu")) {
const found = scanRootForHide(dd.shadowRoot) ?? scanRootForHide(dd);
if (found) return found;
}
const fromBody = scanRootForHide(document.body);
if (fromBody) return fromBody;
for (const om of document.querySelectorAll(OVERFLOW_MENU_SELECTOR)) {
if (om === overflowMenu) continue;
const found = scanRootForHide(om.shadowRoot) ?? scanRootForHide(om);
if (found) return found;
}
for (const bs of document.querySelectorAll("rpl-bottom-sheet")) {
const found = scanRootForHide(bs.shadowRoot) ?? scanRootForHide(bs);
if (found) return found;
}
return null;
}
function dismissOpenMenus() {
document.dispatchEvent(
new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true })
);
}
const VEIL_TARGETS = "faceplate-dropdown-menu, shreddit-post-overflow-menu, rpl-bottom-sheet";
// *** CHANGED: added display: none to prevent any flicker ***
const VEIL_CSS = `
faceplate-dropdown-menu,
shreddit-post-overflow-menu,
rpl-bottom-sheet,
faceplate-dropdown-menu *,
shreddit-post-overflow-menu *,
rpl-bottom-sheet * {
display: none !important;
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
transition: none !important;
animation: none !important;
}
`;
function applyInlineVeil(el) {
el.style.setProperty("display", "none", "important");
el.style.setProperty("opacity", "0", "important");
el.style.setProperty("visibility", "hidden", "important");
el.style.setProperty("pointer-events", "none", "important");
el.style.setProperty("transition", "none", "important");
el.style.setProperty("animation", "none", "important");
}
async function hidePost(post) {
const overflowMenu = post.querySelector(OVERFLOW_MENU_SELECTOR);
if (!overflowMenu) return;
// Veil injected first — kills visibility immediately via CSS,
// plus a MutationObserver as a fallback for dynamically inserted nodes.
const veil = document.createElement("style");
veil.textContent = VEIL_CSS;
document.head.appendChild(veil);
const hideObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (!(node instanceof HTMLElement)) continue;
if (node.matches?.(VEIL_TARGETS)) applyInlineVeil(node);
for (const el of node.querySelectorAll(VEIL_TARGETS)) {
applyInlineVeil(el);
}
}
}
});
hideObserver.observe(document.body, { childList: true, subtree: true });
const cleanup = () => {
veil.remove();
hideObserver.disconnect();
};
dismissOpenMenus();
await new Promise((r) => setTimeout(r, 120));
const triggerBtn =
overflowMenu.querySelector('button[aria-label="Open user actions"]') ??
overflowMenu.shadowRoot?.querySelector('button[aria-label="Open user actions"]') ??
overflowMenu.querySelector("button") ??
overflowMenu.shadowRoot?.querySelector("button");
if (!triggerBtn) { cleanup(); return; }
triggerBtn.click();
await new Promise((r) => setTimeout(r, 250));
const deadline = Date.now() + 3000;
while (Date.now() < deadline) {
const item = findHideMenuItem(overflowMenu);
if (item) {
item.click();
cleanup();
return;
}
await new Promise((r) => setTimeout(r, 60));
}
cleanup();
dismissOpenMenus();
}
// ---------------------------------------------------------------------------
// UI — inject the Hide button next to posts
// ---------------------------------------------------------------------------
function applyButtonStyles(button) {
button.className = BUTTON_CLASS_NAME;
button.style.height = "var(--size-button-sm-h)";
button.style.font = "var(--font-button-sm)";
button.style.display = "inline-flex";
button.style.verticalAlign = "middle";
button.style.removeProperty("margin-inline-end");
}
function createHideButton(post) {
const button = document.createElement("button");
button.type = "button";
button.setAttribute(HIDE_BUTTON_ATTR, "true");
applyButtonStyles(button);
button.textContent = "Hide";
button.title = 'Hide post (hotkey: "h")';
button.addEventListener("click", async (event) => {
event.preventDefault();
event.stopPropagation();
try {
await hidePost(post);
} catch (error) {
console.error("[Reddit Hide Posts] Failed:", error);
}
});
return button;
}
function removeFallbackButtons(post) {
post.shadowRoot?.querySelectorAll(`[${HIDE_BUTTON_ATTR}]`).forEach((b) => b.remove());
}
function ensureTopRightButtons(post) {
const overflowMenu = post.querySelector(OVERFLOW_MENU_SELECTOR);
if (!overflowMenu) return false;
const overflowLoader = overflowMenu.closest("shreddit-async-loader");
const insertionAnchor = overflowLoader || overflowMenu;
const rightActions = insertionAnchor.parentElement;
if (!(rightActions instanceof HTMLElement)) return false;
[...post.querySelectorAll(`[${TOP_BUTTONS_ATTR}]`)].forEach((row) => {
if (row.parentElement !== rightActions) row.remove();
});
let buttonsRow = [...rightActions.children].find(
(child) => child instanceof HTMLElement && child.hasAttribute(TOP_BUTTONS_ATTR)
) || null;
if (!(buttonsRow instanceof HTMLElement)) {
buttonsRow = document.createElement("span");
buttonsRow.setAttribute(TOP_BUTTONS_ATTR, "true");
buttonsRow.style.cssText = "display:inline-flex;flex-direction:row;align-items:center;flex-wrap:nowrap;gap:var(--spacer-2xs);margin-inline-end:var(--spacer-2xs);";
}
if (buttonsRow.parentElement !== rightActions) {
insertionAnchor.insertAdjacentElement("beforebegin", buttonsRow);
}
if (!buttonsRow.querySelector(`[${HIDE_BUTTON_ATTR}]`)) {
buttonsRow.append(createHideButton(post));
}
[...post.querySelectorAll(`[${HIDE_BUTTON_ATTR}]`)].forEach((b) => {
if (!buttonsRow.contains(b)) b.remove();
});
removeFallbackButtons(post);
return true;
}
function ensureSearchResultButtons(post) {
if (!isSearchResultPost(post)) return false;
const card = post.querySelector(SEARCH_POST_UNIT_SELECTOR);
const content = card?.querySelector(SEARCH_POST_CONTENT_SELECTOR);
const titleEl = content?.querySelector('a[data-testid="post-title-text"]');
if (!(card instanceof HTMLElement) || !(content instanceof HTMLElement) || !(titleEl instanceof HTMLElement)) return false;
let buttonsRow = content.querySelector(`[${TOP_BUTTONS_ATTR}]`);
if (!(buttonsRow instanceof HTMLElement)) {
buttonsRow = document.createElement("span");
buttonsRow.setAttribute(TOP_BUTTONS_ATTR, "true");
buttonsRow.style.cssText = "display:inline-flex;flex-direction:row;align-items:center;flex-wrap:nowrap;gap:var(--spacer-2xs);position:relative;z-index:1;pointer-events:auto;flex-shrink:0;";
}
let titleWrapper = titleEl.parentElement;
if (titleWrapper.getAttribute("data-codex-title-wrapper") !== "true") {
titleWrapper = document.createElement("div");
titleWrapper.setAttribute("data-codex-title-wrapper", "true");
titleWrapper.style.cssText = "display:flex;flex-direction:row;align-items:flex-start;justify-content:space-between;gap:var(--spacer-md);margin-bottom:var(--spacer-xs);width:100%;";
titleEl.style.marginBottom = "0";
titleEl.style.flex = "1 1 auto";
titleEl.style.minWidth = "0";
titleEl.insertAdjacentElement("beforebegin", titleWrapper);
titleWrapper.appendChild(titleEl);
}
if (buttonsRow.parentElement !== titleWrapper) titleWrapper.appendChild(buttonsRow);
if (!buttonsRow.querySelector(`[${HIDE_BUTTON_ATTR}]`)) {
buttonsRow.append(createHideButton(post));
}
[...post.querySelectorAll(`[${HIDE_BUTTON_ATTR}]`)].forEach((b) => {
if (!buttonsRow.contains(b)) b.remove();
});
return true;
}
function ensureButtons(post) {
if (ensureSearchResultButtons(post)) return;
if (ensureTopRightButtons(post)) return;
const actionRow = post.shadowRoot?.querySelector(ACTION_ROW_SELECTOR);
if (actionRow && !actionRow.querySelector(`[${HIDE_BUTTON_ATTR}]`)) {
actionRow.append(createHideButton(post));
}
}
function enhancePost(post) {
if (!(post instanceof HTMLElement) || !post.matches(ALL_POST_SELECTOR)) return;
ensureButtons(post);
}
function enhanceAllPosts() {
document.querySelectorAll(ALL_POST_SELECTOR).forEach(enhancePost);
}
function scheduleEnhance() {
if (scheduledEnhance) return;
scheduledEnhance = true;
window.requestAnimationFrame(() => {
scheduledEnhance = false;
enhanceAllPosts();
});
}
// ---------------------------------------------------------------------------
// Event listeners and observers
// ---------------------------------------------------------------------------
document.addEventListener("pointermove", (event) => {
const post = findPostFromEvent(event);
setHoveredPost(post ?? null);
}, true);
document.addEventListener("keydown", (event) => {
if (event.defaultPrevented || event.repeat || event.ctrlKey || event.altKey || event.metaKey) return;
if (event.key.toLowerCase() !== "h") return;
if (isEditableTarget(event.target)) return;
if (!hoveredState.post) return;
event.preventDefault();
hidePost(hoveredState.post);
}, true);
new MutationObserver(scheduleEnhance).observe(document.documentElement, {
childList: true,
subtree: true,
});
enhanceAllPosts();
window.setInterval(enhanceAllPosts, 2000);
})();