SimpCity - Open Unread Alerts in Tabs

Adds a button next to "Alerts" to open all unread alerts in new tabs with a delay to prevent rate limiting.

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

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

Necesitarás 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.

Necesitará instalar una extensión como Tampermonkey para 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)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

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

// ==UserScript==
// @name         SimpCity - Open Unread Alerts in Tabs
// @namespace    http://tampermonkey.net/
// @version      0.2.1
// @description  Adds a button next to "Alerts" to open all unread alerts in new tabs with a delay to prevent rate limiting.
// @author       bitter.beer
// @match        https://simpcity.cr/*
// @match        https://simpcity.is/*
// @match        https://simpcity.cz/*
// @match        https://simpcity.hk/*
// @match        https://simpcity.rs/*
// @match        https://simpcity.ax/*
// @grant        GM_openInTab
// @connect      simpcity.cr
// @connect      simpcity.is
// @connect      simpcity.cz
// @connect      simpcity.hk
// @connect      simpcity.rs
// @connect      simpcity.ax
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  // ===== CONFIG =====
  // Delay (in ms) between opening each unread alert
  const OPEN_DELAY_MS = 3000; // 1 second — adjust as needed

  // Max number of unread alerts to open per click (set to null for all)
  const MAX_TO_OPEN = null; // e.g. 10 to limit to 10 alerts

  // ===== CORE LOGIC =====

  // Adaptive opening queue state (must be defined before usage)
  let isProcessingQueue = false;
  let waitingForNext = false;
  let currentQueueTimer = null;

  /**
   * Find all unread alert links (the actual thread/post links).
   * These are the `.fauxBlockLink-blockLink` anchors inside `.alert.is-unread` items.
   */
  function getUnreadAlertLinks() {
    const selector =
      ".js-alertsMenuBody li.alert.is-unread .fauxBlockLink-blockLink";
    const links = Array.from(document.querySelectorAll(selector));
    return links;
  }

  /**
   * Opens all unread alert links in new tabs with a delay between each.
   */
  function openUnreadAlerts() {
    const links = getUnreadAlertLinks();

    if (!links.length) {
      alert("No unread alerts found.");
      return;
    }

    const totalToOpen = MAX_TO_OPEN
      ? Math.min(MAX_TO_OPEN, links.length)
      : links.length;

    let index = 0;

    // Initialize queue state
    isProcessingQueue = true;
    waitingForNext = false;
    if (currentQueueTimer) {
      clearTimeout(currentQueueTimer);
      currentQueueTimer = null;
    }

    function openNext() {
      // Finished queue
      if (index >= totalToOpen) {
        isProcessingQueue = false;
        waitingForNext = false;
        currentQueueTimer = null;
        return;
      }

      const link = links[index++];
      if (!link || !link.href) {
        openNext();
        return;
      }

      const url = link.href;

      try {
        if (typeof GM_openInTab === "function") {
          GM_openInTab(url, { active: false, insert: true, setParent: true });
        } else {
          window.open(url, "_blank");
        }
      } catch (e) {
        console.error("Failed to open alert tab:", e);
      }

      // Prepare for next opening (either early via message or fallback timer)
      if (index < totalToOpen) {
        waitingForNext = true;
        currentQueueTimer = setTimeout(() => {
          // Fallback path if tab load is slower than delay
          if (!waitingForNext) return; // Already handled by early open
          waitingForNext = false;
          openNext();
        }, OPEN_DELAY_MS);
      } else {
        // Queue will finish after last open
        isProcessingQueue = false;
      }
    }

    // Expose openNext so message handler can trigger early advance
    window.__scOpenNext = openNext;

    openNext();
  }

  /**
   * Ensure the "Open Unread" button exists only when there are unread alerts.
   * Creates the button if needed, removes it if none remain.
   */
  function ensureUnreadButtonForAlertsHeader(headerEl) {
    if (!headerEl) return;
    const existingBtn = headerEl.querySelector("button[data-unread-open-btn]");
    const unreadCount = getUnreadAlertLinks().length;

    if (unreadCount === 0) {
      if (existingBtn) existingBtn.remove();
      return;
    }

    if (existingBtn) return; // Already present and unread exist

    const btn = document.createElement("button");
    btn.type = "button";
    btn.textContent = "Open Unread";
    btn.title = "Open all unread alerts in new tabs (with delay)";
    btn.setAttribute("data-unread-open-btn", "1");
    btn.style.marginLeft = "0.5em";
    btn.style.fontSize = "0.9em";
    btn.style.cursor = "pointer";
    btn.style.padding = "2px 6px";
    btn.style.borderRadius = "3px";
    btn.style.border = "1px solid rgba(255,255,255,0.2)";
    btn.style.background = "hsla(187, 73%, 52%, 0.5)";

    btn.addEventListener("click", function (e) {
      e.stopPropagation();
      openUnreadAlerts();
    });

    headerEl.appendChild(btn);
  }

  /**
   * Find the Alerts header(s) and ensure button visibility accordingly.
   */
  function updateAlertsHeaders(root = document) {
    const headers = root.querySelectorAll("h3.menu-header");
    headers.forEach((h3) => {
      if (h3.textContent.trim() === "Alerts") {
        ensureUnreadButtonForAlertsHeader(h3);
      }
    });
  }

  // Try immediately (in case the menu is already in DOM)
  updateAlertsHeaders(document);

  // ===== MutationObserver to handle dynamically inserted alerts menu =====
  const observer = new MutationObserver((mutationList) => {
    for (const mutation of mutationList) {
      mutation.addedNodes.forEach((node) => {
        if (!(node instanceof HTMLElement)) return;

        // If a chunk of menu content is added, search within it
        if (
          node.matches(".js-alertsMenuBody, .menu, .menu-content") ||
          node.querySelector(".js-alertsMenuBody")
        ) {
          updateAlertsHeaders(node);
        }

        // General fallback: if any h3.menu-header is added anywhere
        if (
          node.matches("h3.menu-header") ||
          node.querySelector("h3.menu-header")
        ) {
          updateAlertsHeaders(node);
        }
      });
    }
  });

  observer.observe(document.body, { childList: true, subtree: true });

  // Periodically re-check in case read state changes without DOM additions (optional safeguard)
  setInterval(() => updateAlertsHeaders(document), 1000);

  // ===== Adaptive Opening Support =====

  // Listen for load completion messages from child tabs
  window.addEventListener("message", (e) => {
    // Basic validation: expecting our custom type
    if (!e.data || e.data.type !== "SC_TAB_LOADED") return;
    if (!isProcessingQueue || !waitingForNext) return;
    // Early proceed
    waitingForNext = false;
    if (currentQueueTimer) {
      clearTimeout(currentQueueTimer);
      currentQueueTimer = null;
    }
    // Continue queue immediately
    // Slight micro-delay to allow browser idle time
    // Proceed immediately (micro-delay not strictly needed)
    if (typeof window.__scOpenNext === "function") {
      window.__scOpenNext();
    }
  });
  // Ensure global callback reference exists even before first queue
  if (typeof window.__scOpenNext !== "function") {
    window.__scOpenNext = function () {};
  }

  // In newly opened tabs, notify opener early once load finishes
  if (window.opener) {
    window.addEventListener("load", () => {
      try {
        window.opener.postMessage({ type: "SC_TAB_LOADED" }, "*");
      } catch (err) {
        // Ignore
      }
    });
  }
})();