Reddit Free Awards – Posts & Comments (Queued + Toggle)

One-click free awards for posts & comments with floating toggle and rate-limit safe queue

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         Reddit Free Awards – Posts & Comments (Queued + Toggle)
// @namespace    https://tampermonkey.net/reddit-free-awards-toggle-floating
// @author       dapro26
// @version      3.4.2
// @description  One-click free awards for posts & comments with floating toggle and rate-limit safe queue
// @match        https://www.reddit.com/*
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(() => {
  "use strict";

  /* ===================== CONFIG ===================== */

  const STORAGE_KEY = "free_awards_enabled";
  const AWARD_DELAY_MS = 2500; // ⏱️ delay between awards (rate-limit safe)

  const AWARDS = [
    { name: "Heartwarming", id: "award_free_heartwarming", img: "https://i.redd.it/snoovatar/snoo_assets/marketing/Heartwarming_40.png" },
    { name: "Popcorn", id: "award_free_popcorn_2", img: "https://i.redd.it/snoovatar/snoo_assets/marketing/Popcorn_40.png" },
    { name: "Bravo", id: "award_free_bravo", img: "https://i.redd.it/snoovatar/snoo_assets/marketing/bravo_40.png" },
    { name: "Regret", id: "award_free_regret_2", img: "https://i.redd.it/snoovatar/snoo_assets/marketing/regret_40.png" },
    { name: "Mindblown", id: "award_free_mindblown", img: "https://i.redd.it/snoovatar/snoo_assets/marketing/mindblown_40.png" },
  ];

  /* ===================== STATE ===================== */

  const isEnabled = () => localStorage.getItem(STORAGE_KEY) !== "false";
  const setEnabled = (v) => localStorage.setItem(STORAGE_KEY, String(v));

  /* ===================== AWARD QUEUE ===================== */

  const awardQueue = [];
  let isProcessing = false;

  const processQueue = async () => {
    if (isProcessing || awardQueue.length === 0) return;
    isProcessing = true;

    const { thingId, awardId, btn, img } = awardQueue.shift();

    try {
      await sendAward(thingId, awardId);
      btn.textContent = "✓";
      btn.style.background = "#4BB543";
      btn.style.color = "white";
    } catch {
      btn.textContent = "✕";
      btn.style.background = "#EA0027";
      btn.style.color = "white";
      setTimeout(() => {
        btn.textContent = "";
        btn.appendChild(img);
        btn.style.background = "transparent";
        btn.style.color = "";
        btn.disabled = false;
      }, 2000);
    }

    setTimeout(() => {
      isProcessing = false;
      processQueue();
    }, AWARD_DELAY_MS);
  };

  /* ===================== HELPERS ===================== */

  const getCurrentUserId = () =>
    document.querySelector("[user-id]")?.getAttribute("user-id") || null;

  const getCsrfToken = () => {
    const c = document.cookie.match(/csrf_token=([^;]+)/);
    if (c) return c[1];
    const app = document.querySelector("shreddit-app");
    return app?.csrfToken || app?.getAttribute("csrf-token") || app?.getAttribute("spp");
  };

  const sendAward = async (thingId, awardId) => {
    const token = getCsrfToken();
    if (!token) throw new Error("No CSRF");

    const res = await fetch("https://www.reddit.com/svc/shreddit/graphql", {
      method: "POST",
      headers: { "Content-Type": "application/json", "X-Csrf-Token": token },
      body: JSON.stringify({
        operation: "CreateAwardOrder",
        variables: { input: { nonce: crypto.randomUUID(), thingId, awardId, isAnonymous: false } },
        csrf_token: token,
      }),
    });

    const json = await res.json();
    if (!json?.data?.createAwardOrder?.ok) throw new Error("Award failed");
  };

  /* ===================== UI ===================== */

  const createAwardButton = (award, thingId) => {
    const btn = document.createElement("button");
    btn.title = award.name;

    btn.style.cssText = `
      background: transparent;
      border: none;
      border-radius: 999px;
      cursor: pointer;
      width: 28px;
      height: 28px;
      display: flex;
      align-items: flex-end;
      justify-content: center;
      padding: 0;
    `;

    const img = document.createElement("img");
    img.src = award.img;
    img.style.cssText = "width:20px;height:20px;margin-bottom:2px;pointer-events:none;";
    btn.appendChild(img);

    btn.onclick = (e) => {
      e.preventDefault();
      e.stopPropagation();
      if (btn.disabled) return;

      btn.disabled = true;
      btn.style.opacity = "0.6";

      awardQueue.push({ thingId, awardId: award.id, btn, img });
      processQueue();
    };

    return btn;
  };

  /* ===================== INJECTION ===================== */

  const removeAllButtons = () =>
    document.querySelectorAll(".free-award-wrap").forEach(e => e.remove());

  const injectCommentButtons = () => {
    document.querySelectorAll("shreddit-comment-action-row").forEach(row => {
      if (!isEnabled() || row.querySelector(".free-award-wrap")) return;

      const comment = row.closest("shreddit-comment");
      if (!comment || comment.hasAttribute("is-author")) return;

      const thingId = row.getAttribute("comment-id");
      if (!thingId) return;

      const wrap = document.createElement("div");
      wrap.className = "free-award-wrap";
      wrap.setAttribute("slot", "comment-award");
      wrap.style.cssText = "display:inline-flex;gap:2px;margin-left:8px;";

      AWARDS.forEach(a => wrap.appendChild(createAwardButton(a, thingId)));

      const existing = row.querySelector('[slot="comment-award"]');
      existing ? existing.after(wrap) : row.appendChild(wrap);
    });
  };

const injectPostButtons = (uid) => {
  document.querySelectorAll("shreddit-post").forEach(post => {
    if (!isEnabled() || post.dataset.freeAwardInjected) return;
    if (post.getAttribute("author-id") === uid) return;

    const thingId = post.id || post.getAttribute("postid");
    if (!thingId) return;

    const wrap = document.createElement("div");
    wrap.className = "free-award-wrap";
    wrap.style.cssText = "display:inline-flex;gap:4px;margin-left:8px;";

    AWARDS.forEach(a => wrap.appendChild(createAwardButton(a, thingId)));

    let target =
      post.querySelector('[slot="post-award"]') ||
      post.querySelector("award-button");

    // 🔑 SHADOW DOM FALLBACK
    if (!target && post.shadowRoot) {
      target =
        post.shadowRoot.querySelector('[slot="post-award"]') ||
        post.shadowRoot.querySelector("award-button");
    }

    if (target) {
      target.after(wrap);
      post.dataset.freeAwardInjected = "true";
    }
  });
};

  /* ===================== FLOATING TOGGLE ===================== */

  const injectToggle = () => {
    if (document.querySelector("#free-award-toggle")) return;

    const btn = document.createElement("button");
    btn.id = "free-award-toggle";

    btn.style.cssText = `
      position: fixed;
      bottom: 20px;
      right: 20px;
      z-index: 999999;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 10px 16px;
      height: 36px;
      border-radius: 999px;
      font-size: 12px;
      font-weight: 700;
      border: none;
      cursor: pointer;
      box-shadow: 0 4px 12px rgba(0,0,0,0.25);
      color: white;
    `;

    const update = () => {
      btn.textContent = `Free Awards: ${isEnabled() ? "ON" : "OFF"}`;
      btn.style.background = isEnabled() ? "#FF4500" : "#555";
    };

    btn.onclick = () => {
      setEnabled(!isEnabled());
      removeAllButtons();
      update();
      run();
    };

    update();
    document.body.appendChild(btn);
  };

  /* ===================== INIT ===================== */

  const run = () => {
    const uid = getCurrentUserId();
    if (!uid) return;
    injectToggle();
    injectCommentButtons();
    injectPostButtons(uid);
  };

  run();
  new MutationObserver(run).observe(document.body, { childList: true, subtree: true });
})();