SimpCity Replies

Adds a "View Replies" button under each post with improved performance and styling.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

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

(I already have a user script manager, let me install it!)

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.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         SimpCity Replies
// @namespace    http://tampermonkey.net/
// @version      0.3.1
// @description  Adds a "View Replies" button under each post with improved performance and styling.
// @author       remuru
// @match        https://simpcity.cr/*
// @match        https://simpcity.su/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      simpcity.cr
// @connect      simpcity.su
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Constants ---
    const BUTTON_TEXT_VIEW = "View Replies";
    const BUTTON_TEXT_HIDE = "Hide Replies";
    const LOADING_TEXT_BASE = "Loading replies";
    const NO_REPLIES_TEXT = "<strong>No replies found.</strong>";
    const ERROR_PREFIX = '<strong style="color: red;">Error:</strong>';

    let fullDomain = window.location.hostname;
    let domainParts = fullDomain.split('.');
    let mainDomain = domainParts.slice(-2).join('.');

    // --- CSS Styling ---
    GM_addStyle(`
        .sc-replies-button {
            margin-top: 10px;
            cursor: pointer;
            /* Add other button styles as needed - using default browser/site styles for now */
            padding: 5px 10px;
            border: 1px solid #555;
            background-color: #444;
            color: #ddd;
            border-radius: 3px;
        }
        .sc-replies-button:hover {
            background-color: #555;
        }
        .sc-replies-container {
            margin-top: 10px;
            border: 1px solid #444;
            padding: 10px;
            background-color: #2e2e2e; /* Slightly different background */
            border-radius: 3px;
        }
        .sc-replies-container[data-loading="true"] strong {
            display: inline-block; /* Needed for animation */
        }
        .sc-replies-loading-dots::after {
            display: inline-block;
            animation: sc-ellipsis 1.25s infinite;
            content: ".";
            width: 1em;
            text-align: left;
        }
        @keyframes sc-ellipsis {
            0% { content: "."; }
            33% { content: ".."; }
            66% { content: "..."; }
        }
        .sc-replies-table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 10px;
        }
        .sc-replies-table th, .sc-replies-table td {
            padding: 8px;
            border: 1px solid #4a4a4a; /* Slightly adjusted border */
            text-align: left;
        }
        .sc-replies-table th {
            background: #3a3a3a; /* Adjusted header background */
            text-align: center;
            font-weight: bold;
        }
        .sc-replies-table tr:nth-child(even) {
            background-color: #333; /* Zebra striping */
        }
         .sc-replies-table td:nth-child(1), /* Post Num */
         .sc-replies-table td:nth-child(2)  /* Date */
         {
            text-align: center;
            white-space: nowrap; /* Prevent date/number wrapping */
         }
         .sc-replies-table th:nth-child(1), /* Post Num Header */
         .sc-replies-table th:nth-child(2)  /* Date Header */
         {
             width: 12%;
         }
         .sc-replies-table th:nth-child(3) { /* Reply Header */
             width: 76%;
             text-align: left;
         }
        .sc-replies-table a {
             color: #4b9dff; /* Link color */
        }
        .sc-replies-table a:hover {
             text-decoration: underline;
        }
    `);

    // --- Helper Functions ---

    const extractThreadId = (postHeaderLink) => {
        // Regex remains the same, seems specific enough
        const match = postHeaderLink?.match(/\.([0-9]+)\/post-/);
        return match ? match[1] : null;
    };

    const convertLinks = (text) => {
        if (!text) return '';
        // Basic URL regex, find http/https links
        const urlRegex = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
        // Use textContent first to prevent potential XSS if source has raw HTML
        const safeText = document.createElement('div');
        safeText.textContent = text;
        // Replace URLs in the safe text
        return safeText.innerHTML.replace(urlRegex, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`);
    };

    const createLoadingContainer = (parent) => {
        const container = document.createElement('div');
        container.className = 'sc-replies-container';
        container.dataset.loading = "true"; // Mark as loading

        const loadingText = document.createElement('strong');
        loadingText.textContent = LOADING_TEXT_BASE;
        loadingText.className = 'sc-replies-loading-dots'; // Add class for CSS animation
        container.appendChild(loadingText);

        parent.appendChild(container);
        return container;
    };

    const showError = (container, message) => {
        container.innerHTML = `${ERROR_PREFIX} ${message}`;
        container.dataset.loading = "false"; // Ensure loading state is cleared on error
    };

    const fetchAnswers = (searchURL, container) => {
        console.log("Fetching replies from:", searchURL);
        GM_xmlhttpRequest({
            method: "GET",
            url: searchURL,
            onload: (response) => {
                container.dataset.loading = "false"; // Stop loading animation

                if (response.status >= 200 && response.status < 300) {
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, "text/html");
                    // Use a more specific selector if possible, but this seems standard for XenForo search results
                    const answerBlocks = doc.querySelectorAll("li.block-row.block-row--separated.js-inlineModContainer[data-author]");

                    if (!answerBlocks || answerBlocks.length === 0) {
                        container.innerHTML = NO_REPLIES_TEXT;
                        return;
                    }

                    // Clear loading text
                    container.innerHTML = "";

                    // Create table structure
                    const table = document.createElement("table");
                    table.className = "sc-replies-table";
                    table.innerHTML = `
                        <thead>
                            <tr>
                                <th>Post #</th>
                                <th>Date</th>
                                <th>Reply Snippet</th>
                            </tr>
                        </thead>`;
                    const tbody = document.createElement("tbody");
                    const fragment = document.createDocumentFragment(); // Use fragment for efficiency

                    answerBlocks.forEach((block) => {
                        // Use optional chaining ?. for safer access
                        const postLink = block.querySelector('.contentRow-main a[href*="/post-"]');
                        const postTime = block.querySelector('time.u-dt');
                        const contentSnippet = block.querySelector('.contentRow-snippet');

                        // Extract post number more reliably from the link itself if possible
                        let postIdText = 'N/A';
                        const postIdMatch = postLink?.href?.match(/\/post-(\d+)/);
                        if (postIdMatch) {
                            postIdText = `#${postIdMatch[1]}`;
                        } else {
                             // Fallback to the list item if needed (less reliable)
                             const postIdElement = block.querySelector('.contentRow-minor.contentRow-minor--hideLinks li:nth-child(2)'); // Example: adjust if needed
                             if (postIdElement) postIdText = postIdElement.textContent.trim();
                        }


                        if (postLink?.href && postTime && contentSnippet) {
                            const row = document.createElement("tr");

                            // Post number (link)
                            const postIdCell = document.createElement("td");
                            const linkElement = document.createElement("a");
                            linkElement.href = postLink.href;
                            linkElement.textContent = postIdText;
                            linkElement.target = "_blank";
                            linkElement.rel = "noopener noreferrer";
                            postIdCell.appendChild(linkElement);

                            // Date
                            const timeCell = document.createElement("td");
                            timeCell.textContent = postTime.textContent.trim();
                            timeCell.title = postTime.getAttribute('datetime') || postTime.getAttribute('data-time-string') || ''; // Add tooltip with full date if available

                            // Reply content
                            const contentCell = document.createElement("td");
                            // Use convertLinks for safety and functionality
                            contentCell.innerHTML = convertLinks(contentSnippet.textContent.trim());

                            row.append(postIdCell, timeCell, contentCell);
                            fragment.appendChild(row); // Append row to fragment
                        } else {
                            console.warn("Skipping reply block, missing required elements:", block);
                        }
                    });

                    tbody.appendChild(fragment); // Append fragment to tbody
                    table.appendChild(tbody);
                    container.appendChild(table);

                } else {
                    showError(container, `Failed to load replies. Status: ${response.status}`);
                }
            },
            onerror: (error) => {
                console.error("GM_xmlhttpRequest error:", error);
                showError(container, "Network error while loading replies.");
                container.dataset.loading = "false";
            },
            ontimeout: () => {
                 showError(container, "Request timed out while loading replies.");
                 container.dataset.loading = "false";
            }
        });
    };

    const addAnswerButton = (postElement) => {
        const mainContainer = postElement.querySelector('.message-main.js-quickEditTarget') || postElement;
        if (!mainContainer) return; // Cannot add button if container not found

        // Check if button already exists
        if (mainContainer.querySelector('.sc-replies-button')) {
            return;
        }

        const btn = document.createElement('button');
        btn.textContent = BUTTON_TEXT_VIEW;
        btn.className = "sc-replies-button";

        mainContainer.appendChild(btn);

        btn.addEventListener('click', () => {
            let repliesContainer = postElement.querySelector('.sc-replies-container');

            if (repliesContainer) {
                // Toggle visibility
                const isHidden = repliesContainer.style.display === 'none';
                repliesContainer.style.display = isHidden ? 'block' : 'none';
                btn.textContent = isHidden ? BUTTON_TEXT_HIDE : BUTTON_TEXT_VIEW;
                return;
            }

            // --- Create container and fetch data ---
            repliesContainer = createLoadingContainer(mainContainer);
            btn.textContent = BUTTON_TEXT_HIDE; // Set text immediately

            // Find post ID (using data-content attribute often present on XenForo posts)
            const messageContent = postElement.closest('.message[data-content]');
            const postId = messageContent?.getAttribute('data-content')?.replace('post-', '');
            // Fallback to lb-id if data-content is not present
            const lbContainer = !postId ? postElement.querySelector('[data-lb-id]') : null;
            const postIdFallback = lbContainer?.getAttribute('data-lb-id')?.replace('post-', '');
            const finalPostId = postId || postIdFallback;

            if (!finalPostId) {
                showError(repliesContainer, "Could not determine Post ID.");
                btn.textContent = BUTTON_TEXT_VIEW; // Revert button text on immediate error
                btn.disabled = true; // Disable button if critical info missing
                return;
            }

            // Find thread ID
            const headerLink = postElement.querySelector('.message-attribution-main a[href*="/threads/"]');
            const threadId = extractThreadId(headerLink?.getAttribute("href"));

            if (!threadId) {
                showError(repliesContainer, "Could not determine Thread ID.");
                btn.textContent = BUTTON_TEXT_VIEW;
                btn.disabled = true;
                return;
            }

            const searchId = "1"; // Potentially fragile hardcoded value
            const searchURL = `https://${mainDomain}/search/${searchId}/?q=post-${finalPostId}&t=post&c[thread]=${threadId}&o=date`; // Added &o=date to sort by date (optional)

            fetchAnswers(searchURL, repliesContainer);
        });
    };

    // --- Main Execution ---

    // Function to process posts currently on the page
    const processPosts = () => {
        document.querySelectorAll(".message.message--post .message-inner").forEach(addAnswerButton);
    };

    // Initial run
    processPosts();


})();