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