Reddit Corner Buttons (Saved Posts & Messages) + Reddit AD Button Hider

Adds buttons to access Saved Posts and Messages (bottom-right corner) and hides the 'Advertise' button/link on Reddit using its ID. Handles SPA navigation and iframe issues.

// ==UserScript==
// @name         Reddit Corner Buttons (Saved Posts & Messages) + Reddit AD Button Hider
// @namespace    Https://github.com/ctrlcmdshft/
// @version      2.6.1
// @description  Adds buttons to access Saved Posts and Messages (bottom-right corner) and hides the 'Advertise' button/link on Reddit using its ID. Handles SPA navigation and iframe issues.
// @author       CtrlCmdShft
// @match        https://www.reddit.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=reddit.com
// @noframes     Crucial: Prevents the script from running in iframes, avoiding duplicate runs on pages like Messages.
// @homepageURL  https://github.com/ctrlcmdshft/RedditQuickAccess
// @homepage     https://github.com/ctrlcmdshft/RedditQuickAccess
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-start // Run early to set the global flag and inject styles.
// @license      MIT // Example license, feel free to change
// ==/UserScript==

(function() {
    'use strict';

    /* --- Configuration --- */

    // Constants for button IDs and URLs
    const SAVED_URL = 'https://www.reddit.com/user/me/saved';
    const MESSAGES_URL = 'https://www.reddit.com/message/inbox'; // Base URL for messages page check
    const EXPORT_URL = 'https://www.reddit.com/user/me/comments';
    const SAVED_BUTTON_ID = 'userscript-reddit-saved-button';
    const MESSAGES_BUTTON_ID = 'userscript-reddit-messages-button';
    const ADVERTISE_BUTTON_ID = 'advertise-button'; 
    const EXPORT_COMMENTS_BUTTON_ID = 'userscript-reddit-export-comments-button'; // Add this
    const COMMENTS_PER_REQUEST = 100;
    const MAX_RETRIES = 3;

    const EXPORT_FORMATS = {
        txt: 'Text (.txt)',
        json: 'JSON (.json)',
        csv: 'CSV (.csv)'
    };

    // SVG Icons for the buttons
    const BOOKMARK_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg>`;
    const MAIL_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="currentColor" aria-hidden="true"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6zm-2 0l-8 5-8-5h16zm0 12H4V8l8 5 8-5v10z"/></svg>`;
    const EXPORT_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>`; // Add this

    /* --- Settings Management --- */
    const DEFAULT_SETTINGS = {
        showSavedButton: true,
        showMessagesButton: true,
        showExportButton: true
    };

    function loadSettings() {
        return {
            showSavedButton: GM_getValue('showSavedButton', DEFAULT_SETTINGS.showSavedButton),
            showMessagesButton: GM_getValue('showMessagesButton', DEFAULT_SETTINGS.showMessagesButton),
            showExportButton: GM_getValue('showExportButton', DEFAULT_SETTINGS.showExportButton)
        };
    }

    function saveSettings(settings) {
        GM_setValue('showSavedButton', settings.showSavedButton);
        GM_setValue('showMessagesButton', settings.showMessagesButton);
        GM_setValue('showExportButton', settings.showExportButton);
    }

    function toggleFeature(feature) {
        const settings = loadSettings();
        settings[feature] = !settings[feature];
        saveSettings(settings);
        initializeOrRefreshButtons(); // Refresh buttons to reflect changes
    }

    // Register menu commands
    GM_registerMenuCommand('Toggle Saved Button', () => toggleFeature('showSavedButton'));
    GM_registerMenuCommand('Toggle Messages Button', () => toggleFeature('showMessagesButton'));
    GM_registerMenuCommand('Toggle Export Button', () => toggleFeature('showExportButton'));

    /* --- Initialization Guard (Global Flag) --- */

    const GLOBAL_FLAG_NAME = '__redditCornerButtonsInitialized_v2_5__'; // Use a versioned name
    if (window[GLOBAL_FLAG_NAME]) {
        // console.log(`Reddit Corner Buttons & Ad Hider: Global flag ${GLOBAL_FLAG_NAME} found. Aborting secondary execution.`);
        return;
    }
    window[GLOBAL_FLAG_NAME] = true;
    // console.log(`Reddit Corner Buttons & Ad Hider: Global flag ${GLOBAL_FLAG_NAME} set by this instance.`);


    /* --- Styling --- */

    // CSS styles for the buttons and ad hiding.
    const styles = `
        /* Corner Buttons Base Style */
        .userscript-corner-button {
            position: fixed;
            right: 20px;
            bottom: 20px; /* Default bottom position */
            z-index: 1001;
            width: 40px;
            height: 40px;
            color: #ffffff;
            border: none;
            border-radius: 50%;
            cursor: pointer;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            transition: all 0.3s ease; /* Smooth transition for position changes */
            display: flex !important;
            align-items: center;
            justify-content: center;
            padding: 0;
        }
        .userscript-corner-button svg {
            width: 20px;
            height: 20px;
        }

        /* Saved Posts Button */
        #${SAVED_BUTTON_ID} {
            background-color: #ff4500;
        }
        #${SAVED_BUTTON_ID}:hover {
            background-color: #ff5722;
        }

        /* Messages Button */
        #${MESSAGES_BUTTON_ID} {
            background-color: #0079D3;
        }
        #${MESSAGES_BUTTON_ID}:hover {
            background-color: #1484D7;
        }

        /* Export Comments Button */
        #${EXPORT_COMMENTS_BUTTON_ID} {
            background-color: #1a9131;
        }
        #${EXPORT_COMMENTS_BUTTON_ID}:hover {
            background-color: #23ad3c;
        }

        /* Dynamic positioning when both buttons are present */
        body:has(#${SAVED_BUTTON_ID}):has(#${MESSAGES_BUTTON_ID}) #${MESSAGES_BUTTON_ID} {
            bottom: 70px;
        }
        body:has(#${SAVED_BUTTON_ID}):has(#${MESSAGES_BUTTON_ID}) #${SAVED_BUTTON_ID} {
            bottom: 20px;
        }

        /* Update dynamic positioning for three buttons */
        body:has(#${SAVED_BUTTON_ID}):has(#${MESSAGES_BUTTON_ID}):has(#${EXPORT_COMMENTS_BUTTON_ID}) #${EXPORT_COMMENTS_BUTTON_ID} {
            bottom: 120px;
        }
        body:has(#${SAVED_BUTTON_ID}):has(#${MESSAGES_BUTTON_ID}):has(#${EXPORT_COMMENTS_BUTTON_ID}) #${MESSAGES_BUTTON_ID} {
            bottom: 70px;
        }

        /* --- Hide the 'Advertise' Button using its ID --- */
        #${ADVERTISE_BUTTON_ID} {
            display: none !important;
        }

        /* Export Format Menu */
        .export-format-menu {
            background: #1a9131;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
            padding: 8px;
            display: flex;
            flex-direction: column;
            gap: 4px;
            z-index: 1002;
            min-width: 120px;
            animation: menuFadeIn 0.2s ease;
        }

        .export-format-menu button {
            background: none;
            border: none;
            padding: 8px 16px;
            text-align: left;
            cursor: pointer;
            border-radius: 4px;
            color: #ffffff;
            font-size: 14px;
            white-space: nowrap;
            transition: background-color 0.2s ease;
        }

        .export-format-menu button:hover {
            background-color: #23ad3c;
        }

        /* Dark mode support */
        @media (prefers-color-scheme: dark) {
            .export-format-menu {
                background: #1a9131;
            }
            
            .export-format-menu button {
                color: #ffffff;
            }
            
            .export-format-menu button:hover {
                background-color: #23ad3c;
            }
        }
    `;
    // Inject the styles into the page head.
    try {
        GM_addStyle(styles);
        // console.log("Reddit Corner Buttons & Ad Hider: Styles injected.");
    } catch (e) {
        console.error("Reddit Corner Buttons & Ad Hider: Failed to inject styles using GM_addStyle.", e);
    }


    /* --- Core Functions --- */

    async function fetchComments(after = null, retryCount = 0) {
        try {
            const url = `https://www.reddit.com/user/me/comments.json?limit=${COMMENTS_PER_REQUEST}${after ? '&after=' + after : ''}`;
            const response = await fetch(url);
            if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
            const data = await response.json();
            return data;
        } catch (error) {
            if (retryCount < MAX_RETRIES) {
                await new Promise(resolve => setTimeout(resolve, 1000 * (retryCount + 1)));
                return fetchComments(after, retryCount + 1);
            }
            throw error;
        }
    }

    async function getAllComments() {
        let allComments = [];
        let after = null;
        
        try {
            do {
                const data = await fetchComments(after);
                const comments = data.data.children;
                allComments = allComments.concat(comments);
                after = data.data.after;
            } while (after);
            
            return allComments;
        } catch (error) {
            console.error('Error fetching comments:', error);
            throw error;
        }
    }

    function formatCommentsForExport(comments) {
        return comments.map(comment => {
            const date = new Date(comment.data.created_utc * 1000);
            const formattedDate = date.toLocaleString();
            
            // Format the comment body - replace markdown links and clean up newlines
            let body = comment.data.body
                .replace(/\[(.*?)\]\((.*?)\)/g, '$1 ($2)') // Convert markdown links to readable format
                .replace(/\n{3,}/g, '\n\n')                // Replace multiple newlines with double newline
                .trim();

            return {
                subreddit: `r/${comment.data.subreddit}`,
                date: formattedDate,
                score: comment.data.score,
                body: body,
                context: `https://reddit.com${comment.data.permalink}?context=3`,
                post_title: comment.data.link_title || '[Title not available]'
            };
        });
    }

    function downloadComments(comments, format = 'txt') {  // Changed default to 'txt'
        const formattedComments = formatCommentsForExport(comments);
        let content, filename, type;

        if (format === 'txt') {
            // Enhanced text format for better readability
            const header = `Reddit Comments Export\n` +
                          `Generated: ${new Date().toLocaleString()}\n` +
                          `Total Comments: ${formattedComments.length}\n` +
                          `${'='.repeat(60)}\n\n`;

            const commentsContent = formattedComments.map((c, index) => (
                `Comment #${index + 1}\n` +
                `${'='.repeat(30)}\n` +
                `Posted in: ${c.subreddit}\n` +
                `Date: ${c.date}\n` +
                `Score: ${c.score}\n` +
                `Post: ${c.post_title}\n` +
                `\nComment:\n${'~'.repeat(20)}\n${c.body}\n${'~'.repeat(20)}\n` +
                `\nLink: ${c.context}\n` +
                `\n${'='.repeat(60)}\n`
            )).join('\n');

            content = header + commentsContent;
            filename = `reddit-comments-${new Date().toISOString().split('T')[0]}.txt`;
            type = 'text/plain';
        } else if (format === 'json') {
            // Make JSON more readable with better spacing and structure
            content = JSON.stringify({
                export_date: new Date().toLocaleString(),
                total_comments: formattedComments.length,
                comments: formattedComments
            }, null, 2);
            filename = 'reddit-comments.json';
            type = 'application/json';
        } else if (format === 'csv') {
            // Make CSV more readable with proper escaping and formatting
            const headers = ['Subreddit', 'Date', 'Score', 'Post Title', 'Comment', 'Link'];
            const rows = formattedComments.map(c => [
                c.subreddit,
                c.date,
                c.score,
                c.post_title.replace(/"/g, '""'),
                c.body.replace(/"/g, '""').replace(/\n/g, ' '),
                c.context
            ].map(field => `"${field}"`).join(','));
            
            content = [headers.join(','), ...rows].join('\n');
            filename = 'reddit-comments.csv';
            type = 'text/csv';
        }

        const blob = new Blob([content], { type: `${type};charset=utf-8` });
        const url = URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.download = filename;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        URL.revokeObjectURL(url);
    }

    /* --- Update the createFormatMenu function --- */
    function createFormatMenu(button, onSelect) {
        const menu = document.createElement('div');
        menu.className = 'export-format-menu';
        
        // Add format options
        Object.entries(EXPORT_FORMATS).forEach(([format, label]) => {
            const option = document.createElement('button');
            option.textContent = label;
            option.onclick = () => {
                onSelect(format);
                menu.remove();
            };
            menu.appendChild(option);
        });

        // Position menu next to the button with smart placement
        const buttonRect = button.getBoundingClientRect();
        const menuPadding = 10; // Space between button and menu
        
        menu.style.position = 'fixed';
        menu.style.right = `${window.innerWidth - buttonRect.left + menuPadding}px`;
        
        // Append menu to get its dimensions
        document.body.appendChild(menu);
        const menuRect = menu.getBoundingClientRect();
        
        // Check if menu would go off screen at the top
        if (buttonRect.top - menuRect.height < 0) {
            // Position below the button if not enough space above
            menu.style.top = `${buttonRect.bottom + menuPadding}px`;
        } else {
            // Position above the button
            menu.style.top = `${buttonRect.top - menuRect.height - menuPadding}px`;
        }

        // Update styles for the menu
        const styles = `
            /* Update Export Format Menu styles */
            .export-format-menu {
                position: fixed;
                background: #ffffff;
                border-radius: 8px;
                box-shadow: 0 2px 10px rgba(0,0,0,0.2);
                padding: 8px;
                display: flex;
                flex-direction: column;
                gap: 4px;
                z-index: 1002;
                min-width: 120px;
                animation: menuFadeIn 0.2s ease;
            }

            @keyframes menuFadeIn {
                from {
                    opacity: 0;
                    transform: translateY(5px);
                }
                to {
                    opacity: 1;
                    transform: translateY(0);
                }
            }

            .export-format-menu button {
                background: none;
                border: none;
                padding: 8px 16px;
                text-align: left;
                cursor: pointer;
                border-radius: 4px;
                color: #1a1a1b;
                font-size: 14px;
                white-space: nowrap;
                transition: background-color 0.2s ease;
            }

            .export-format-menu button:hover {
                background-color: #f6f7f8;
            }

            /* Dark mode support */
            @media (prefers-color-scheme: dark) {
                .export-format-menu {
                    background: #1a1a1b;
                    border: 1px solid #343536;
                }
                
                .export-format-menu button {
                    color: #ffffff;
                }
                
                .export-format-menu button:hover {
                    background-color: #272729;
                }
            }
        `;

        // Add or update styles
        const existingStyle = document.getElementById('export-menu-styles');
        if (!existingStyle) {
            const styleElement = document.createElement('style');
            styleElement.id = 'export-menu-styles';
            styleElement.textContent = styles;
            document.head.appendChild(styleElement);
        }
        
        // Close menu when clicking outside
        const closeMenu = (e) => {
            if (!menu.contains(e.target) && !button.contains(e.target)) {
                menu.remove();
                document.removeEventListener('click', closeMenu);
            }
        };
        
        // Delay adding click listener to prevent immediate closure
        setTimeout(() => document.addEventListener('click', closeMenu), 0);
        
        return menu;
    }

    /**
     * Creates a button element based on provided options.
     * @param {object} options - Button properties (id, title, svgHTML, url).
     * @returns {HTMLButtonElement|null} The created button element or null on error.
     */
    function createButtonElement(options) {
        try {
            const button = document.createElement('button');
            button.id = options.id;
            button.className = 'userscript-corner-button';
            button.title = options.title;
            button.setAttribute('aria-label', options.title);
            button.innerHTML = options.svgHTML;

            if (options.id === EXPORT_COMMENTS_BUTTON_ID) {
                button.onclick = async function() {
                    const existingMenu = document.querySelector('.export-format-menu');
                    if (existingMenu) {
                        existingMenu.remove();
                        return;
                    }

                    createFormatMenu(button, async (format) => {
                        button.style.opacity = '0.5';
                        button.style.cursor = 'wait';
                        try {
                            const comments = await getAllComments();
                            downloadComments(comments, format);
                        } catch (error) {
                            alert('Failed to export comments. Please try again.');
                        } finally {
                            button.style.opacity = '1';
                            button.style.cursor = 'pointer';
                        }
                    });
                };
            } else {
                button.onclick = function() {
                    window.location.href = options.url;
                };
            }
            return button;
        } catch (e) {
            console.error(`Reddit Corner Buttons & Ad Hider: Error creating button element ${options.id}:`, e);
            return null;
        }
    }

    /**
     * Removes existing corner buttons from the DOM.
     */
    function removeExistingButtons() {
        const idsToRemove = [SAVED_BUTTON_ID, MESSAGES_BUTTON_ID, EXPORT_COMMENTS_BUTTON_ID];
        // console.log("Reddit Corner Buttons & Ad Hider: Checking for and removing existing buttons...");
        idsToRemove.forEach(id => {
            const existingButton = document.getElementById(id);
            if (existingButton) {
                // console.log(` - Removing button with ID: ${id}`);
                existingButton.remove();
            }
        });
    }

    /**
     * Ensures corner buttons are present. Removes existing ones and adds new ones.
     */
    function initializeOrRefreshButtons() {
        if (!document.body) {
             console.error("Reddit Corner Buttons & Ad Hider: initializeOrRefreshButtons called but document.body not found!");
             return;
        }

        // Remove potentially lingering buttons
        removeExistingButtons();

        // Load current settings
        const settings = loadSettings();

        // Define button configurations with visibility checks
        const buttonConfigs = [
            settings.showSavedButton && {
                id: SAVED_BUTTON_ID,
                title: 'View Saved Posts',
                svgHTML: BOOKMARK_ICON_SVG,
                url: SAVED_URL
            },
            settings.showMessagesButton && {
                id: MESSAGES_BUTTON_ID,
                title: 'View Messages',
                svgHTML: MAIL_ICON_SVG,
                url: MESSAGES_URL
            },
            settings.showExportButton && {
                id: EXPORT_COMMENTS_BUTTON_ID,
                title: 'Export Comments',
                svgHTML: EXPORT_ICON_SVG,
                url: EXPORT_URL
            }
        ].filter(Boolean); // Remove false values

        // Create and append each button
        buttonConfigs.forEach(config => {
            const buttonElement = createButtonElement(config);
            if (buttonElement) {
                try {
                    document.body.appendChild(buttonElement);
                } catch(e) {
                    console.error(`Reddit Corner Buttons & Ad Hider: Failed to append button ${config.id} to body:`, e);
                }
            }
        });
    }


    /* --- Initialization Trigger --- */

    // Logic for initializing corner buttons (no changes needed here for the ad hiding part)
    const isOnMessagesPageInitially = window.location.href.startsWith(MESSAGES_URL);

    if (isOnMessagesPageInitially) {
        // Special Handling for Messages Page
        // console.log("Reddit Corner Buttons & Ad Hider: On messages page. Waiting for 'load' event.");
        if (document.readyState === 'complete') {
            // console.log("Reddit Corner Buttons & Ad Hider: 'load' event already complete, running initialization now.");
            initializeOrRefreshButtons();
        } else {
            window.addEventListener('load', initializeOrRefreshButtons);
        }
    } else {
        // Standard Handling for Other Pages
        // console.log("Reddit Corner Buttons & Ad Hider: Not on messages page. Waiting for 'DOMContentLoaded'.");
        if (document.readyState === 'interactive' || document.readyState === 'complete') {
            // console.log("Reddit Corner Buttons & Ad Hider: 'DOMContentLoaded' already complete, running initialization now.");
            initializeOrRefreshButtons();
        } else {
            window.addEventListener('DOMContentLoaded', initializeOrRefreshButtons);
        }
    }


    /* --- Global Flag Cleanup --- */

    window.addEventListener('beforeunload', () => {
        // console.log(`Reddit Corner Buttons & Ad Hider: Clearing global flag ${GLOBAL_FLAG_NAME} on beforeunload.`);
        delete window[GLOBAL_FLAG_NAME];
    });

})(); // End of UserScript IIFE