Bluesky Auto-Open Spoilers

Automatically opens spoilers and removes warnings on Bluesky (bsky.app) for feed and individual posts

// ==UserScript==
// @name         Bluesky Auto-Open Spoilers
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Automatically opens spoilers and removes warnings on Bluesky (bsky.app) for feed and individual posts
// @match        https://bsky.app/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Set to keep track of processed elements to avoid duplicates
    const processedElements = new Set();

    // Throttle function to limit how often processSpoilers runs
    let lastRun = 0;
    const throttleDelay = 500; // Run at most every 500ms

    function throttleProcessSpoilers() {
        const now = Date.now();
        if (now - lastRun >= throttleDelay) {
            processSpoilers();
            lastRun = now;
        }
    }

    // Function to open spoilers and remove warnings
    function processSpoilers() {
        // Select all spoiler buttons in feed items and individual post views
        const spoilerButtons = document.querySelectorAll(
            'div[data-testid^="feedItem-"] button[aria-label*="Suggestive"]:not([data-processed]), ' +
            'div[data-testid^="feedItem-"] button[aria-label*="Spoiler"]:not([data-processed]), ' +
            'div[data-testid^="postThreadItem-"] button[aria-label*="Suggestive"]:not([data-processed]), ' +
            'div[data-testid^="postThreadItem-"] button[aria-label*="Spoiler"]:not([data-processed])'
        );
        
        spoilerButtons.forEach(button => {
            // Mark as processed to avoid reprocessing
            const buttonId = button.getAttribute('data-id') || `${Date.now()}-${Math.random()}`;
            button.setAttribute('data-id', buttonId);
            button.setAttribute('data-processed', 'true');
            if (processedElements.has(buttonId)) return;
            processedElements.add(buttonId);

            if (!button.getAttribute('aria-pressed') || button.getAttribute('aria-pressed') === 'false') {
                button.click(); // Reveal the spoiler
            }

            // Find the parent post container (feed item or thread item)
            let postContainer = button.closest('div[data-testid^="feedItem-"]') || button.closest('div[data-testid^="postThreadItem-"]');
            if (postContainer) {
                // Remove the entire spoiler button
                button.remove();

                // Find the content container with the image
                let contentContainer = postContainer.querySelector('div[data-expoimage="true"]');
                if (contentContainer) {
                    // Replace the parent content hider with just the image content
                    let contentHider = postContainer.querySelector('div[data-testid="contentHider-post"]');
                    if (contentHider) {
                        contentHider.innerHTML = contentContainer.outerHTML;
                    } else {
                        // If contentHider is not found, replace the button's parent div
                        let buttonParent = button.closest('div[style*="overflow: hidden"]');
                        if (buttonParent) {
                            buttonParent.innerHTML = contentContainer.outerHTML;
                        }
                    }
                }
            }
        });
    }

    // Run the function initially, with a delay for direct loads, and on page navigation
    throttleProcessSpoilers();
    setTimeout(throttleProcessSpoilers, 500); // Handle delayed DOM rendering on direct loads

    // Handle page load or navigation to individual posts
    window.addEventListener('load', throttleProcessSpoilers);
    window.addEventListener('popstate', throttleProcessSpoilers); // Handle back/forward navigation

    // Set up a MutationObserver to handle dynamically loaded content
    const observer = new MutationObserver((mutations) => {
        throttleProcessSpoilers();
    });

    // Observe the document for changes (e.g., new posts or page updates)
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    // Clean up the observer when the script is unloaded
    window.addEventListener('unload', () => {
        observer.disconnect();
        processedElements.clear();
    });
})();