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.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

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