8chan Hiding Enhancer

Hides backlinks pointing to hidden posts, prevents hover tooltips and adds strikethrough to quotelinks, and adds recursive hiding/filtering options on 8chan.moe/se. Also adds unhiding options.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         8chan Hiding Enhancer
// @namespace    nipah-scripts-8chan
// @version      1.5.1
// @description  Hides backlinks pointing to hidden posts, prevents hover tooltips and adds strikethrough to quotelinks, and adds recursive hiding/filtering options on 8chan.moe/se. Also adds unhiding options.
// @author       nipah, Gemini
// @license      MIT
// @match        https://8chan.moe/*/res/*.html*
// @match        https://8chan.se/*/res/*.html*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const SCRIPT_NAME = 'Hiding Enhancer';
    const BACKLINK_SELECTOR = '.panelBacklinks a, .altBacklinks a';
    const QUOTE_LINK_SELECTOR = '.quoteLink';
    const ALL_LINK_SELECTORS = `${BACKLINK_SELECTOR}, ${QUOTE_LINK_SELECTOR}`;
    const POST_CONTAINER_SELECTOR = '.opCell, .postCell';
    const INNER_POST_SELECTOR = '.innerOP, .innerPost'; // Selector for the inner content div
    const THREAD_CONTAINER_SELECTOR = '#divThreads'; // Container for all posts in the thread
    const HIDDEN_CLASS = 'hidden'; // Class added to the container when hidden by hiding.js
    const HIDDEN_QUOTE_CLASS = 'hidden-quote'; // Class to add to quote links for hidden posts
    const TOOLTIP_SELECTOR = '.quoteTooltip'; // Selector for the tooltip element
    const HIDE_BUTTON_SELECTOR = '.hideButton'; // Selector for the hide menu button
    const HIDE_MENU_SELECTOR = '.floatingList.extraMenu'; // Selector for the hide menu dropdown
    const LABEL_ID_SELECTOR = '.labelId'; // Selector for the post ID label
    const UNHIDE_BUTTON_SELECTOR = '.unhideButton'; // Selector for the site's unhide button

    const log = (...args) => console.log(`[${SCRIPT_NAME}]`, ...args);
    const warn = (...args) => console.warn(`[${SCRIPT_NAME}]`, ...args);
    const error = (...args) => console.error(`[${SCRIPT_NAME}]`, ...args);


    let debounceTimer = null;
    const DEBOUNCE_DELAY = 250; // ms


    /**
     * Injects custom CSS styles into the document head.
     */
    function addCustomStyles() {
        const style = document.createElement('style');
        style.type = 'text/css';
        style.innerHTML = `
            .${HIDDEN_QUOTE_CLASS} {
                text-decoration: line-through !important;
            }
            /* Style for the dynamically added menu items */
            ${HIDE_MENU_SELECTOR} li[data-action^="hide-recursive"],
            ${HIDE_MENU_SELECTOR} li[data-action^="filter-id-recursive"],
            ${HIDE_MENU_SELECTOR} li[data-action="show-id"],
            ${HIDE_MENU_SELECTOR} li[data-action="show-all"] {
                cursor: pointer;
            }
        `;
        document.head.appendChild(style);
        log('Custom styles injected.');
    }

    /**
     * Extracts the target post ID from a link's href attribute.
     * Works for both backlinks and quote links.
     * @param {HTMLAnchorElement} linkElement - The link <a> element.
     * @returns {string|null} The target post ID as a string, or null if not found.
     */
    function getTargetPostIdFromLink(linkElement) {
        if (!linkElement || !linkElement.href) {
            return null;
        }
        // Match the post number after the last '#'
        const match = linkElement.href.match(/#(\d+)$/);
        // Only return numeric post ID
        return match ? match[1] : null;
    }

    /**
     * Checks if a post is currently hidden based on its ID.
     * @param {string} postId - The ID of the post to check.
     * @returns {boolean} True if the post is hidden, false otherwise.
     */
    function isPostHidden(postId) {
        if (!postId) return false;
        const postContainer = document.getElementById(postId);
        if (!postContainer) return false;

        // Check if the main container (.opCell or .postCell) is hidden (can happen with thread hiding)
        if (postContainer.classList.contains(HIDDEN_CLASS)) {
            return true;
        }

        // Check if the inner content container (.innerOP or .innerPost) is hidden (common for post hiding)
        const innerContent = postContainer.querySelector(INNER_POST_SELECTOR);
        return innerContent ? innerContent.classList.contains(HIDDEN_CLASS) : false;
    }

    /**
     * Updates the visibility or style of a single link based on its target post's hidden status.
     * Handles both backlinks and quote links.
     * @param {HTMLAnchorElement} linkElement - The link <a> element to update.
     */
    function updateLinkVisibility(linkElement) {
        const targetPostId = getTargetPostIdFromLink(linkElement);
        // Ensure it's a numeric post ID link
        if (!targetPostId) return;

        const hidden = isPostHidden(targetPostId);

        if (linkElement.classList.contains('quoteLink')) {
            // It's a quote link, apply strikethrough
            if (hidden) {
                linkElement.classList.add(HIDDEN_QUOTE_CLASS);
                // // log(`Adding strikethrough to quote link ${linkElement.href} pointing to hidden post ${targetPostId}`);
            } else {
                linkElement.classList.remove(HIDDEN_QUOTE_CLASS);
                // // log(`Removing strikethrough from quote link ${linkElement.href} pointing to visible post ${targetPostId}`);
            }
        } else {
            // It's a backlink, hide/show the element
            if (hidden) {
                linkElement.style.display = 'none';
                // // log(`Hiding backlink ${linkElement.href} pointing to hidden post ${targetPostId}`);
            } else {
                // Reset display.
                linkElement.style.display = '';
                // // log(`Showing backlink ${linkElement.href} pointing to visible post ${targetPostId}`);
            }
        }
    }

    /**
     * Iterates through all relevant links (backlinks and quote links) on the page and updates their visibility/style.
     */
    function updateAllLinks() {
        log('Updating all link visibility/style...');
        const links = document.querySelectorAll(ALL_LINK_SELECTORS);
        links.forEach(updateLinkVisibility);
        log(`Checked ${links.length} links.`);
    }

    /**
     * Debounced version of updateAllLinks.
     */
    function debouncedUpdateAllLinks() {
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(updateAllLinks, DEBOUNCE_DELAY);
    }

    /**
     * Overrides the site's tooltips.loadTooltip function to prevent tooltips for hidden posts.
     */
    function overrideLoadTooltip() {
        // Check if tooltips object and loadTooltip function exist
        if (typeof tooltips === 'undefined' || typeof tooltips.loadTooltip !== 'function') {
            // Not ready, try again later
            setTimeout(overrideLoadTooltip, 100);
            return;
        }

        const originalLoadTooltip = tooltips.loadTooltip;

        tooltips.loadTooltip = function(tooltip, quoteUrl, replyId, isInline) {
            // Only intercept hover tooltips (isInline is false for hover tooltips)
            if (!isInline) {
                const matches = quoteUrl.match(/\/(\w+)\/res\/(\d+)\.html\#(\d+)/);
                const targetPostId = matches ? matches[3] : null; // Post ID is the 3rd group

                if (targetPostId && isPostHidden(targetPostId)) {
                    log(`Preventing hover tooltip for quote to hidden post ${targetPostId}`);
                    // Remove the tooltip element that was just created by the site's code
                    if (tooltip && tooltip.parentNode) {
                        tooltip.parentNode.removeChild(tooltip);
                    }
                    // Clear the site's internal reference if it points to the removed tooltip
                    // This is important so tooltips.removeIfExists doesn't try to remove it again
                    if (tooltips.currentTooltip === tooltip) {
                         tooltips.currentTooltip = null;
                    }
                    // Prevent the original function from running
                    return;
                }
            }

            // If it's an inline quote OR the target post is not hidden, call the original function
            originalLoadTooltip.apply(this, arguments);
        };

        log('tooltips.loadTooltip overridden to prevent tooltips for hidden posts.');
    }

    /**
     * Implements the recursive hiding logic using the site's hiding.hidePost function.
     * Hides a specific post and all its replies recursively.
     * @param {string} startPostId - The ID of the post to start hiding from.
     */
    function hidePostAndRepliesRecursivelyUserscript(startPostId) {
        // Ensure site objects are available
        if (typeof hiding === 'undefined' || typeof hiding.hidePost !== 'function' || typeof tooltips === 'undefined' || typeof tooltips.knownPosts === 'undefined') {
            error('Site hiding or tooltips objects not available. Cannot perform recursive hide.');
            return;
        }

        const boardUri = window.location.pathname.split('/')[1]; // Get boardUri from URL

        function recursiveHide(currentPostId) {
            const postElement = document.getElementById(currentPostId);
            if (!postElement) {
                // Post element not found (might be filtered or not loaded)
                // log(`Post element ${currentPostId} not found.`);
                return;
            }

            // Check if the post already has the site's unhide button, indicating it's already hidden
            if (postElement.querySelector(UNHIDE_BUTTON_SELECTOR)) {
                 // log(`Post ${currentPostId} is already hidden (unhide button found). Skipping.`);
            } else {
                 const linkSelf = postElement.querySelector('.linkSelf');
                 if (linkSelf) {
                     log(`Hiding post ${currentPostId}`);
                     // Call the site's hidePost function
                     hiding.hidePost(linkSelf);
                 } else {
                     warn(`Could not find .linkSelf for post ${currentPostId}. Cannot hide.`);
                 }
            }

            // Find replies using the site's tooltips.knownPosts structure
            const knownPost = tooltips.knownPosts[boardUri]?.[currentPostId];

            if (!knownPost || !knownPost.added || knownPost.added.length === 0) {
                // log(`No known replies for post ${currentPostId}. Stopping recursion.`);
                return; // No replies or post not found in knownPosts
            }

            // Recursively hide replies
            knownPost.added.forEach((replyString) => {
                const [replyBoard, replyId] = replyString.split('_');

                // Only hide replies within the same board and thread
                // The site's knownPosts structure seems to only track replies within the same thread anyway
                if (replyBoard === boardUri) {
                     recursiveHide(replyId);
                }
            });
        }

        // Start the recursive hiding process
        log(`Starting recursive hide from post ${startPostId}`);
        recursiveHide(startPostId);
        log(`Finished recursive hide from post ${startPostId}`);

        // After hiding is done, trigger a link update to reflect changes
        debouncedUpdateAllLinks();
    }

    /**
     * Implements the recursive filtering logic.
     * Adds an ID filter and recursively hides replies for all posts matching that ID.
     * @param {string} targetId - The raw ID string (e.g., '0feed1') to filter by.
     * @param {string} clickedPostId - The ID of the post whose menu was clicked (used for context/logging).
     */
    function filterIdAndHideAllMatchingAndReplies(targetId, clickedPostId) {
         // Ensure site objects are available
        if (typeof settingsMenu === 'undefined' || typeof settingsMenu.createFilter !== 'function' || typeof hiding === 'undefined' || typeof hiding.hidePost !== 'function' || typeof tooltips === 'undefined' || typeof tooltips.knownPosts === 'undefined' || typeof hiding.buildPostFilterId !== 'function') {
            error('Site settingsMenu, hiding, tooltips, or hiding.buildPostFilterId objects not available. Cannot perform recursive ID filter.');
            return;
        }

        const boardUri = window.location.pathname.split('/')[1];
        const threadId = window.location.pathname.split('/')[3].split('.')[0]; // Extract thread ID from URL

        // Find the linkSelf element for the clicked post to pass to buildPostFilterId
        const clickedPostElement = document.getElementById(clickedPostId);
        let formattedFilterString = targetId; // Fallback to raw ID

        if (clickedPostElement) {
             const linkSelf = clickedPostElement.querySelector('.linkSelf');
             if (linkSelf) {
                 // Use the site's function to get the formatted ID string
                 formattedFilterString = hiding.buildPostFilterId(linkSelf, targetId);
             } else {
                 warn(`Could not find .linkSelf for clicked post ${clickedPostId}. Using raw ID for filter.`);
             }
        } else {
             warn(`Could not find clicked post element ${clickedPostId}. Using raw ID for filter.`);
        }


        log(`Applying Filter ID++ for ID: ${targetId} (formatted as "${formattedFilterString}") triggered from post ${clickedPostId})`);

        // 1. Add the ID filter using the site's function
        // Type 4 is for filtering by ID
        settingsMenu.createFilter(formattedFilterString, false, 4);
        log(`Added filter for ID: ${formattedFilterString}`);

        // Give the site's filter logic a moment to apply the 'hidden' class
        // Then find all posts with this ID and recursively hide their replies
        setTimeout(() => {
            const allPosts = document.querySelectorAll(POST_CONTAINER_SELECTOR);

            allPosts.forEach(postElement => {
                const postIdLabel = postElement.querySelector(LABEL_ID_SELECTOR);
                const currentPostId = postElement.id;

                // Check if the post matches the target ID
                if (postIdLabel && postIdLabel.textContent === targetId) {
                    log(`Found post ${currentPostId} matching ID ${targetId}. Recursively hiding its replies.`);
                    // Call the recursive hide function starting from this post.
                    // hidePostAndRepliesRecursivelyUserscript will handle hiding the post itself
                    // (if not already hidden by the filter) and its replies.
                    hidePostAndRepliesRecursivelyUserscript(currentPostId);
                }
            });

            // After hiding is done, trigger a link update to reflect changes
            // This is already handled by hidePostAndRepliesRecursivelyUserscript,
            // but calling it again here after the loop ensures all changes are caught.
            debouncedUpdateAllLinks();

        }, DEBOUNCE_DELAY + 50); // Wait slightly longer than the debounce delay
    }

    /**
     * Removes all filters associated with a specific raw ID from the site's settings.
     * @param {string} targetId - The raw ID string (e.g., '0feed1') to remove filters for.
     * @param {string} clickedPostId - The ID of the post whose menu was clicked (used for context/logging).
     */
    function removeIdFilters(targetId, clickedPostId) {
        // Ensure site objects are available
        if (typeof settingsMenu === 'undefined' || typeof settingsMenu.loadedFilters === 'undefined' || typeof hiding === 'undefined' || typeof hiding.checkFilters !== 'function' || typeof hiding.buildPostFilterId !== 'function') {
            error('Site settingsMenu, hiding, or hiding.buildPostFilterId objects not available. Cannot remove ID filters.');
            return;
        }

        const boardUri = window.location.pathname.split('/')[1];
        const threadId = window.location.pathname.split('/')[3].split('.')[0]; // Extract thread ID from URL

        // Find the linkSelf element for the clicked post to pass to buildPostFilterId
        const clickedPostElement = document.getElementById(clickedPostId);
        let formattedFilterString = targetId; // Fallback to raw ID

        if (clickedPostElement) {
             const linkSelf = clickedPostElement.querySelector('.linkSelf');
             if (linkSelf) {
                 // Use the site's function to get the formatted ID string
                 formattedFilterString = hiding.buildPostFilterId(linkSelf, targetId);
             } else {
                 warn(`Could not find .linkSelf for clicked post ${clickedPostId}. Using raw ID for filter removal check.`);
             }
        } else {
             warn(`Could not find clicked post element ${clickedPostId}. Using raw ID for filter removal check.`);
        }

        log(`Attempting to remove filters for ID: ${targetId} (formatted as "${formattedFilterString}") triggered from post ${clickedPostId})`);

        // Filter out the matching filters
        const initialFilterCount = settingsMenu.loadedFilters.length;
        settingsMenu.loadedFilters = settingsMenu.loadedFilters.filter(filter => {
            // Check if it's an ID filter (type 4 or 5) and if the filter content matches the formatted ID string
            return !( (filter.type === 4 || filter.type === 5) && filter.filter === formattedFilterString );
        });

        const removedCount = initialFilterCount - settingsMenu.loadedFilters.length;

        if (removedCount > 0) {
            log(`Removed ${removedCount} filter(s) for ID: ${formattedFilterString}`);
            // Update localStorage
            localStorage.setItem('filterData', JSON.stringify(settingsMenu.loadedFilters));
            // Trigger the site's filter update
            hiding.checkFilters();
            log('Triggered site filter update.');
        } else {
            log(`No filters found for ID: ${formattedFilterString} to remove.`);
        }

        // After removing filters, trigger a link update to reflect changes (posts might become visible)
        debouncedUpdateAllLinks();
    }

    /**
     * Removes all ID filters and manual hides for the current thread.
     */
    function showAllInThread() {
        // Ensure site objects are available
        if (typeof settingsMenu === 'undefined' || typeof settingsMenu.loadedFilters === 'undefined' || typeof hiding === 'undefined' || typeof hiding.checkFilters !== 'function' || typeof hiding.buildPostFilterId !== 'function') {
            error('Site settingsMenu, hiding, or hiding.buildPostFilterId objects not available. Cannot show all in thread.');
            return;
        }

        const boardUri = window.location.pathname.split('/')[1];
        const threadId = window.location.pathname.split('/')[3].split('.')[0]; // Extract thread ID from URL

        log(`Attempting to show all posts in thread /${boardUri}/res/${threadId}.html`);

        let filtersRemoved = 0;
        let unhideButtonsClicked = 0;

        // 1. Find and click all existing unhide buttons in the current thread
        log('Searching for and clicking existing unhide buttons...');
        const allPostsInThread = document.querySelectorAll(POST_CONTAINER_SELECTOR);
        allPostsInThread.forEach(postElement => {
            const postId = postElement.id;
            if (!postId) return; // Skip if element has no ID

            let unhideButton = null;
            if (postId === threadId) {
                // For the thread (OP), the button is the previous sibling
                unhideButton = postElement.previousElementSibling;
                if (!unhideButton || !unhideButton.matches(UNHIDE_BUTTON_SELECTOR)) {
                    unhideButton = null; // Reset if not found or doesn't match
                }
            } else {
                // For regular posts, the button is inside the post container
                unhideButton = postElement.querySelector(UNHIDE_BUTTON_SELECTOR);
            }

            if (unhideButton) {
                log(`Clicking unhide button for ${postId}`);
                unhideButton.click();
                unhideButtonsClicked++;
            }
        });
        log(`Clicked ${unhideButtonsClicked} unhide button(s).`);

        // 2. Remove ID filters specific to this thread from settingsMenu
        const initialFilterCount = settingsMenu.loadedFilters.length;
        settingsMenu.loadedFilters = settingsMenu.loadedFilters.filter(filter => {
            // Check if it's an ID filter (type 4 or 5) and if the filter content starts with the board-thread prefix
            const isThreadIdFilter = (filter.type === 4 || filter.type === 5) && filter.filter.startsWith(`${boardUri}-${threadId}-`);
            if (isThreadIdFilter) {
                filtersRemoved++;
            }
            return !isThreadIdFilter;
        });

        if (filtersRemoved > 0) {
            log(`Removed ${filtersRemoved} ID filter(s) specific to this thread.`);
            // Update localStorage for filters
            localStorage.setItem('filterData', JSON.stringify(settingsMenu.loadedFilters));
        } else {
            log('No ID filters specific to this thread found to remove.');
        }

        // 3. Trigger the site's filter update AFTER a short delay to allow button clicks to process
        //    and for the filter removal to take effect.
        setTimeout(() => {
            hiding.checkFilters();
            log('Triggered site filter update after delay.');

            // 5. Trigger userscript link update
            debouncedUpdateAllLinks();

            log('Finished "Show All" action.');
        }, 100); // 100ms delay
    }


    /**
     * Adds the custom "Hide post++", "Filter ID++", "Show ID", and "Show All" options to a hide button's menu when it appears.
     * Uses a MutationObserver to detect when the menu is added to the button.
     * @param {HTMLElement} hideButton - The hide button element.
     */
    function addCustomHideMenuOptions(hideButton) {
        // Create a new observer for each hide button
        // This observer will stay active for the lifetime of the hideButton element
        const observer = new MutationObserver((mutationsList) => {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    for (const addedNode of mutation.addedNodes) {
                        // Check if the added node is the menu we're looking for
                        if (addedNode.nodeType === Node.ELEMENT_NODE && addedNode.matches(HIDE_MENU_SELECTOR)) {
                            // Check if this menu is a child of the target hideButton
                            if (hideButton.contains(addedNode)) {
                                // Menu appeared, now add the custom options if they're not already there
                                const menuUl = addedNode.querySelector('ul');
                                if (menuUl) {
                                    const postContainer = hideButton.closest(POST_CONTAINER_SELECTOR);
                                    const postId = postContainer ? postContainer.id : null;
                                    const isOP = postContainer ? postContainer.classList.contains('opCell') : false;
                                    const postIdLabel = postContainer ? postContainer.querySelector(LABEL_ID_SELECTOR) : null;
                                    const postIDText = postIdLabel ? postIdLabel.textContent : null;

                                    // Find anchor points for insertion
                                    const hidePostPlusItem = Array.from(menuUl.querySelectorAll('li')).find(li => li.textContent.trim() === 'Hide post+');
                                    const filterIdPlusItem = Array.from(menuUl.querySelectorAll('li')).find(li => li.textContent.trim() === 'Filter ID+');

                                    // Keep track of the last item we inserted after
                                    let lastInsertedAfter = null;

                                    // --- Add "Hide post++" ---
                                    // Only add for reply posts and if it doesn't exist
                                    if (!isOP && postId && !menuUl.querySelector('li[data-action="hide-recursive"]')) {
                                        const hideRecursiveItem = document.createElement('li');
                                        hideRecursiveItem.textContent = 'Hide post++';
                                        hideRecursiveItem.dataset.action = 'hide-recursive';

                                        hideRecursiveItem.addEventListener('click', (event) => {
                                            log(`'Hide post++' clicked for post ${postId}`);
                                            hidePostAndRepliesRecursivelyUserscript(postId);
                                        });

                                        // Insert after "Hide post+" if found
                                        if (hidePostPlusItem) {
                                            hidePostPlusItem.after(hideRecursiveItem);
                                            lastInsertedAfter = hideRecursiveItem;
                                            log(`Added 'Hide post++' option after 'Hide post+' for post ${postId}.`);
                                        } else {
                                            // Fallback: append to the end if "Hide post+" isn't found (shouldn't happen for replies)
                                            menuUl.appendChild(hideRecursiveItem);
                                            lastInsertedAfter = hideRecursiveItem;
                                            warn(`'Hide post+' not found for post ${postId}. Appended 'Hide post++' to end.`);
                                        }
                                    }

                                    // --- Add "Filter ID++" ---
                                    // Only add if the post has an ID and it doesn't exist
                                    if (postIDText && !menuUl.querySelector('li[data-action="filter-id-recursive"]')) {
                                        const filterIdRecursiveItem = document.createElement('li');
                                        filterIdRecursiveItem.textContent = 'Filter ID++';
                                        filterIdRecursiveItem.dataset.action = 'filter-id-recursive';

                                        filterIdRecursiveItem.addEventListener('click', (event) => {
                                            filterIdAndHideAllMatchingAndReplies(postIDText, postId);
                                        });

                                        // Insert after "Filter ID+" if it exists, otherwise after the last item we added ("Hide post++")
                                        if (filterIdPlusItem) {
                                            filterIdPlusItem.after(filterIdRecursiveItem);
                                            lastInsertedAfter = filterIdRecursiveItem;
                                            log(`Added 'Filter ID++' option after 'Filter ID+' for post ${postId}.`);
                                        } else if (lastInsertedAfter) { // If Hide post++ was added
                                            lastInsertedAfter.after(filterIdRecursiveItem);
                                            lastInsertedAfter = filterIdRecursiveItem;
                                            warn(`'Filter ID+' not found for post ${postId}. Appended 'Filter ID++' after last added item.`);
                                        } else {
                                            // Fallback: append to the end if neither "Filter ID+" nor "Hide post++" were present/added
                                            menuUl.appendChild(filterIdRecursiveItem);
                                            lastInsertedAfter = filterIdRecursiveItem;
                                            warn(`Neither 'Filter ID+' nor previous custom item found for post ${postId}. Appended 'Filter ID++' to end.`);
                                        }
                                    }

                                    // --- Add "Show ID" ---
                                    // Only add if the post has an ID and it doesn't exist
                                    if (postIDText && !menuUl.querySelector('li[data-action="show-id"]')) {
                                        const showIdItem = document.createElement('li');
                                        showIdItem.textContent = 'Show ID';
                                        showIdItem.dataset.action = 'show-id';

                                        showIdItem.addEventListener('click', (event) => {
                                            removeIdFilters(postIDText, postId);
                                            // Simulate click outside to close menu via site's logic
                                            setTimeout(() => document.body.click(), 0);
                                        });

                                        // Insert after the last item we added ("Filter ID++" or "Hide post++")
                                        if (lastInsertedAfter) {
                                            lastInsertedAfter.after(showIdItem);
                                            lastInsertedAfter = showIdItem;
                                            log(`Added 'Show ID' option after last added custom item for post ${postId}.`);
                                        } else if (filterIdPlusItem) {
                                             // Fallback if no custom items were added before this, but "Filter ID+" exists
                                             filterIdPlusItem.after(showIdItem);
                                             lastInsertedAfter = showIdItem;
                                             warn(`No previous custom item found for post ${postId}. Appended 'Show ID' after 'Filter ID+'.`);
                                        } else {
                                            // Fallback: append to the end if nothing else was added/found
                                            menuUl.appendChild(showIdItem);
                                            lastInsertedAfter = showIdItem;
                                            warn(`Neither previous custom item nor 'Filter ID+' found for post ${postId}. Appended 'Show ID' to end.`);
                                        }
                                    }

                                    // --- Add "Show All" ---
                                    // Add this option regardless of post type or ID, if it doesn't exist
                                    if (!menuUl.querySelector('li[data-action="show-all"]')) {
                                        const showAllItem = document.createElement('li');
                                        showAllItem.textContent = 'Show All';
                                        showAllItem.dataset.action = 'show-all';

                                        showAllItem.addEventListener('click', (event) => {
                                            log(`'Show All' clicked for post ${postId}`);
                                            showAllInThread();
                                            // Simulate click outside to close menu via site's logic
                                            setTimeout(() => document.body.click(), 0);
                                        });

                                        // Insert after the last item we added ("Show ID", "Filter ID++", or "Hide post++")
                                        if (lastInsertedAfter) {
                                            lastInsertedAfter.after(showAllItem);
                                        } else {
                                            // Fallback: append to the end if no other custom items were added
                                            menuUl.appendChild(showAllItem);
                                        }
                                        log(`Added 'Show All' option for post ${postId}.`);
                                    }


                                } else {
                                    warn('Could not find ul inside hide menu.');
                                }
                            }
                        }
                    }
                }
            }
        });

        // Start observing the hide button for added children (the menu appears as a child)
        observer.observe(hideButton, { childList: true });
    }

    /**
     * Finds all existing hide buttons on the page and attaches the menu observer logic.
     */
    function addCustomHideOptionsToExistingButtons() {
        const hideButtons = document.querySelectorAll(HIDE_BUTTON_SELECTOR);
        hideButtons.forEach(addCustomHideMenuOptions);
        log(`Attached menu observers to ${hideButtons.length} existing hide buttons.`);
    }


    // --- Initialization ---

    log('Initializing...');

    // Add custom CSS styles
    addCustomStyles();

    // Initial setup after a short delay to ensure site scripts are ready
    setTimeout(() => {
        updateAllLinks(); // Update links based on initial hidden posts
        overrideLoadTooltip(); // Override tooltip function
        addCustomHideOptionsToExistingButtons(); // Add menu options to posts already on the page
    }, 500);


    // Observe changes in the thread container to catch new posts or visibility changes
    const threadContainer = document.querySelector(THREAD_CONTAINER_SELECTOR);
    if (threadContainer) {
        const observer = new MutationObserver((mutationsList) => {
            let needsLinkUpdate = false;
            for (const mutation of mutationsList) {
                // Check for class changes on post containers (.opCell, .postCell) or their inner content (.innerOP, .innerPost)
                if (mutation.type === 'attributes' && mutation.attributeName === 'class' && (mutation.target.matches(POST_CONTAINER_SELECTOR) || mutation.target.matches(INNER_POST_SELECTOR))) {
                     const wasHidden = mutation.oldValue ? mutation.oldValue.includes(HIDDEN_CLASS) : false;
                     const isHidden = mutation.target.classList.contains(HIDDEN_CLASS);
                     if (wasHidden !== isHidden) {
                        const postContainer = mutation.target.closest(POST_CONTAINER_SELECTOR);
                        const postId = postContainer ? postContainer.id : 'unknown';
                        log(`Mutation: Class change on post ${postId}. Hidden: ${isHidden}. Triggering link update.`);
                        needsLinkUpdate = true;
                     }
                }
                // Check for new nodes being added
                else if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            // If a post container is added directly
                            if (node.matches(POST_CONTAINER_SELECTOR)) {
                                log(`Mutation: New post container added (ID: ${node.id}). Triggering link update and adding menu observer.`);
                                needsLinkUpdate = true;
                                const hideButton = node.querySelector(HIDE_BUTTON_SELECTOR);
                                if (hideButton) {
                                    addCustomHideMenuOptions(hideButton); // Attach observer to the new hide button
                                }
                            } else {
                                // Check for post containers within the added node's subtree
                                const newPosts = node.querySelectorAll(POST_CONTAINER_SELECTOR);
                                if (newPosts.length > 0) {
                                    log(`Mutation: New posts added within subtree. Triggering link update and adding menu observers.`);
                                    needsLinkUpdate = true;
                                    newPosts.forEach(post => {
                                        const hideButton = post.querySelector(HIDE_BUTTON_SELECTOR);
                                        if (hideButton) {
                                            addCustomHideMenuOptions(hideButton); // Attach observer to new hide buttons
                                        }
                                    });
                                }
                            }
                        }
                    });
                    // Also check removed nodes in case backlinks need updating
                    mutation.removedNodes.forEach(node => {
                         if (node.nodeType === Node.ELEMENT_NODE) {
                            if (node.matches(POST_CONTAINER_SELECTOR) || node.querySelector(POST_CONTAINER_SELECTOR)) {
                                 log(`Mutation: Post removed. Triggering link update.`);
                                 needsLinkUpdate = true;
                            }
                         }
                    });
                }
            }

            if (needsLinkUpdate) {
                debouncedUpdateAllLinks();
            }
        });

        observer.observe(threadContainer, {
            attributes: true,       // Watch for attribute changes (like 'class')
            attributeFilter: ['class'], // Only care about class changes
            attributeOldValue: true,// Need old value to see if 'hidden' changed
            childList: true,        // Watch for new nodes being added or removed
            subtree: true           // Watch descendants (the posts and their inner content)
        });

        log('MutationObserver attached to thread container for link updates and new menu options.');

    } else {
        warn('Thread container not found. Links and menu options will not update automatically on dynamic changes.');
    }

})();