Reddit - Hide Posts

Adds a persistent Hide button to Reddit posts and search results, and lets you press h to hide the post under the cursor.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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();
})();