Greasy Fork is available in English.
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.5.0
// @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 GM_getValue
// @grant GM_setValue
// @license MIT
// ==/UserScript==
(() => {
"use strict";
// Configuration
const HIDDEN_STORAGE_KEY = "reddit-hidden-post-ids:v1";
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 HIDDEN_ATTR = "data-codex-hidden-post";
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;
// ---------------------------------------------------------------------------
// Storage
// ---------------------------------------------------------------------------
function loadStoredIds(storageKey) {
try {
let raw = typeof GM_getValue === "function" ? GM_getValue(storageKey) : null;
if (!raw) {
raw = window.localStorage.getItem(storageKey);
if (raw && typeof GM_setValue === "function") {
GM_setValue(storageKey, raw);
}
}
const parsed = raw ? JSON.parse(raw) : [];
return new Set(Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string" && value) : []);
} catch {
return new Set();
}
}
const hiddenPostIds = loadStoredIds(HIDDEN_STORAGE_KEY);
function persistStoredIds(storageKey, values) {
try {
const stringified = JSON.stringify([...values]);
if (typeof GM_setValue === "function") {
GM_setValue(storageKey, stringified);
}
window.localStorage.setItem(storageKey, stringified);
} catch {
// Ignore storage failures
}
}
function persistHiddenPostIds() {
persistStoredIds(HIDDEN_STORAGE_KEY, hiddenPostIds);
}
// ---------------------------------------------------------------------------
// Core Post Helpers
// ---------------------------------------------------------------------------
function getPostId(post) {
return post?.getAttribute("id") || post?.getAttribute("data-thingid") || post?.getAttribute("permalink") || null;
}
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) {
return;
}
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;
}
// ---------------------------------------------------------------------------
// Hide Functionality
// ---------------------------------------------------------------------------
function applyHiddenState(post) {
const postId = getPostId(post);
const shouldHide = Boolean(postId && hiddenPostIds.has(postId));
if (shouldHide) {
post.setAttribute(HIDDEN_ATTR, "true");
post.style.display = "none";
return;
}
post.removeAttribute(HIDDEN_ATTR);
post.style.removeProperty("display");
}
function showUndoToast(post, postId) {
const existingToast = document.getElementById("codex-undo-toast");
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement("div");
toast.id = "codex-undo-toast";
toast.style.position = "fixed";
toast.style.bottom = "24px";
toast.style.right = "24px";
toast.style.backgroundColor = "var(--color-neutral-background-inverted, #1A1A1B)";
toast.style.color = "var(--color-neutral-content-inverted, #FFFFFF)";
toast.style.padding = "12px 24px";
toast.style.borderRadius = "8px";
toast.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.15)";
toast.style.display = "flex";
toast.style.alignItems = "center";
toast.style.gap = "16px";
toast.style.zIndex = "999999";
toast.style.fontFamily = "inherit";
toast.style.fontSize = "14px";
toast.style.pointerEvents = "auto";
const text = document.createElement("span");
text.textContent = "Post hidden.";
const undoBtn = document.createElement("button");
undoBtn.textContent = "Undo";
undoBtn.style.backgroundColor = "transparent";
undoBtn.style.color = "var(--color-primary-background-default, #0079D3)";
undoBtn.style.border = "none";
undoBtn.style.cursor = "pointer";
undoBtn.style.fontWeight = "bold";
undoBtn.style.padding = "0";
undoBtn.style.fontFamily = "inherit";
undoBtn.style.fontSize = "14px";
let timeoutId;
const removeToast = () => {
if (toast.parentNode) {
toast.remove();
}
};
undoBtn.addEventListener("click", () => {
hiddenPostIds.delete(postId);
persistHiddenPostIds();
applyHiddenState(post);
removeToast();
clearTimeout(timeoutId);
});
toast.appendChild(text);
toast.appendChild(undoBtn);
document.body.appendChild(toast);
timeoutId = window.setTimeout(() => {
removeToast();
}, 4000);
}
function hidePost(post) {
const postId = getPostId(post);
if (!post || !postId) {
return;
}
hiddenPostIds.add(postId);
persistHiddenPostIds();
applyHiddenState(post);
if (hoveredState.post === post) {
setHoveredPost(null);
}
showUndoToast(post, postId);
}
// ---------------------------------------------------------------------------
// UI Integration
// ---------------------------------------------------------------------------
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 createActionButton({ attr, label, title, onClick }) {
const button = document.createElement("button");
button.type = "button";
button.setAttribute(attr, "true");
applyButtonStyles(button);
button.textContent = label;
button.title = title;
button.addEventListener("click", async (event) => {
event.preventDefault();
event.stopPropagation();
try {
await onClick(button);
} catch (error) {
console.error("[Reddit Utilities] Button action failed:", error);
}
});
return button;
}
function createHideButton(post) {
return createActionButton({
attr: HIDE_BUTTON_ATTR,
label: "Hide",
title: 'Hide post (hotkey: "h")',
onClick: async () => {
hidePost(post);
},
});
}
function removeFallbackButtons(post) {
const root = post.shadowRoot;
if (!root) {
return;
}
root.querySelectorAll(`[${HIDE_BUTTON_ATTR}]`).forEach((button) => button.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.display = "inline-flex";
buttonsRow.style.flexDirection = "row";
buttonsRow.style.alignItems = "center";
buttonsRow.style.flexWrap = "nowrap";
buttonsRow.style.gap = "var(--spacer-2xs)";
buttonsRow.style.marginInlineEnd = "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((button) => {
if (!buttonsRow.contains(button)) {
button.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.display = "inline-flex";
buttonsRow.style.flexDirection = "row";
buttonsRow.style.alignItems = "center";
buttonsRow.style.flexWrap = "nowrap";
buttonsRow.style.gap = "var(--spacer-2xs)";
buttonsRow.style.position = "relative";
buttonsRow.style.zIndex = "1";
buttonsRow.style.pointerEvents = "auto";
buttonsRow.style.flexShrink = "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.display = "flex";
titleWrapper.style.flexDirection = "row";
titleWrapper.style.alignItems = "flex-start";
titleWrapper.style.justifyContent = "space-between";
titleWrapper.style.gap = "var(--spacer-md)";
titleWrapper.style.marginBottom = "var(--spacer-xs)";
titleWrapper.style.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((button) => {
if (!buttonsRow.contains(button)) {
button.remove();
}
});
return true;
}
function ensureButtons(post) {
if (ensureSearchResultButtons(post)) {
return;
}
if (ensureTopRightButtons(post)) {
return;
}
const root = post.shadowRoot;
if (!root) {
return;
}
const actionRow = root.querySelector(ACTION_ROW_SELECTOR);
if (!actionRow) {
return;
}
if (!actionRow.querySelector(`[${HIDE_BUTTON_ATTR}]`)) {
actionRow.append(createHideButton(post));
}
}
function enhancePost(post) {
if (!(post instanceof HTMLElement) || !post.matches(ALL_POST_SELECTOR)) {
return;
}
applyHiddenState(post);
if (!post.hasAttribute(HIDDEN_ATTR)) {
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?.hasAttribute(HIDDEN_ATTR) ? null : post);
}, 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);
const observer = new MutationObserver(() => {
scheduleEnhance();
});
function start() {
enhanceAllPosts();
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
window.setInterval(enhanceAllPosts, 2000);
}
start();
})();