Anti Bubble Spam

Merges consecutive chat messages from the same user in Torn.com, with customizable background colors and options. Supports bubble and classic mode.

// ==UserScript==
// @name         Anti Bubble Spam
// @namespace    http://tampermonkey.net/
// @version      6.0
// @description  Merges consecutive chat messages from the same user in Torn.com, with customizable background colors and options. Supports bubble and classic mode.
// @author       Elaine [2047176]
// @match        https://www.torn.com/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Konfiguration ---
    const SCRIPT_ID_PREFIX = 'antiBubbleSpam_';
    const CHAT_ROOT_ID = 'chatRoot';
    const MESSAGE_LIST_SELECTOR = 'div[class*="list___"]';
    const MESSAGE_ITEM_SELECTOR = 'div[class*="item___"]';

    const MESSAGE_ROOT_SELECTOR = `div[class*="list___"] > div[class^="root___"]`;
    const MESSAGE_BOX_SELECTOR = `div[class^="box___"]`;
    const MESSAGE_TEXT_CONTAINER_SELECTOR = `p[class*="message___"], span[class*="message___"]`;

    const SENDER_LINK_SELECTOR = `a[href*="/profiles.php?XID="][class*="sender___"]`;
    const SENDER_CONTAINER_SELECTOR_BUBBLE = `span[class*="senderContainer___"]`;

    const SELF_MESSAGE_CLASS_PART_BUBBLE = 'self___';
    const TIMESTAMP_DIVIDER_SELECTOR = `div[class*="messageGroupTimestamp___"]`;

    const DATA_ORIGINAL_HTML_ATTR = 'data-original-html';
    const SETTINGS_PANEL_SELECTOR = `#settings_panel div[class*="content___"]`;
    const CLASSIC_MODE_LIST_CLASS = 'classic___HVCmX';

    const NO_NEWLINE_FIRST_BUBBLE_AGGREGATOR_CLASS = 'abs-no-newline-bubble-agg';
    const NO_NEWLINE_FIRST_CLASSIC_AGGREGATOR_CLASS = 'abs-no-newline-classic-agg';

    const DEFAULT_SETTINGS = {
        bgColor1_rgb: "0,0,0",
        bgColor1_alpha: 0.1,
        bgColor2_rgb: "255,255,255",
        bgColor2_alpha: 0.05,
        enabled: true,
        startFirstMessageOnNewLine: true,
        useAlternatingBackgrounds: true
    };
    let currentSettings = { ...DEFAULT_SETTINGS };
    let styleElement = null;
    let colorSetting1Div = null;
    let colorSetting2Div = null;


    function isClassicMode(messageElementOrList) {
        const listElement = messageElementOrList.matches(MESSAGE_LIST_SELECTOR)
            ? messageElementOrList
            : messageElementOrList.closest(MESSAGE_LIST_SELECTOR);
        return listElement ? listElement.classList.contains(CLASSIC_MODE_LIST_CLASS) : false;
    }

    function loadSettings() {
        currentSettings.bgColor1_rgb = GM_getValue(SCRIPT_ID_PREFIX + 'bgColor1_rgb', DEFAULT_SETTINGS.bgColor1_rgb);
        currentSettings.bgColor1_alpha = parseFloat(GM_getValue(SCRIPT_ID_PREFIX + 'bgColor1_alpha', DEFAULT_SETTINGS.bgColor1_alpha));
        currentSettings.bgColor2_rgb = GM_getValue(SCRIPT_ID_PREFIX + 'bgColor2_rgb', DEFAULT_SETTINGS.bgColor2_rgb);
        currentSettings.bgColor2_alpha = parseFloat(GM_getValue(SCRIPT_ID_PREFIX + 'bgColor2_alpha', DEFAULT_SETTINGS.bgColor2_alpha));
        currentSettings.enabled = GM_getValue(SCRIPT_ID_PREFIX + 'enabled', DEFAULT_SETTINGS.enabled);
        currentSettings.startFirstMessageOnNewLine = GM_getValue(SCRIPT_ID_PREFIX + 'startFirstMessageOnNewLine', DEFAULT_SETTINGS.startFirstMessageOnNewLine);
        currentSettings.useAlternatingBackgrounds = GM_getValue(SCRIPT_ID_PREFIX + 'useAlternatingBackgrounds', DEFAULT_SETTINGS.useAlternatingBackgrounds);
    }

    function saveSettings() {
        GM_setValue(SCRIPT_ID_PREFIX + 'bgColor1_rgb', currentSettings.bgColor1_rgb);
        GM_setValue(SCRIPT_ID_PREFIX + 'bgColor1_alpha', currentSettings.bgColor1_alpha);
        GM_setValue(SCRIPT_ID_PREFIX + 'bgColor2_rgb', currentSettings.bgColor2_rgb);
        GM_setValue(SCRIPT_ID_PREFIX + 'bgColor2_alpha', currentSettings.bgColor2_alpha);
        GM_setValue(SCRIPT_ID_PREFIX + 'enabled', currentSettings.enabled);
        GM_setValue(SCRIPT_ID_PREFIX + 'startFirstMessageOnNewLine', currentSettings.startFirstMessageOnNewLine);
        GM_setValue(SCRIPT_ID_PREFIX + 'useAlternatingBackgrounds', currentSettings.useAlternatingBackgrounds);
    }

    function hexToRgbString(hex) {
        const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
        return result ? `${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}` : null;
    }

    function rgbStringToHex(rgbString) {
        if (!rgbString) return "#000000";
        const rgbArray = rgbString.split(',').map(Number);
        return "#" + rgbArray.map(x => {
            const hex = x.toString(16);
            return hex.length === 1 ? "0" + hex : hex;
        }).join('');
    }

    function updateStyles() {
        if (!styleElement) {
            styleElement = document.createElement('style');
            styleElement.id = SCRIPT_ID_PREFIX + 'dynamic_styles';
            document.head.appendChild(styleElement);
        }
        if (!currentSettings.enabled) {
            styleElement.innerHTML = ''; // Clear styles if script is disabled
            return;
        }

        let css = `
            .merged-message-wrapper .merged-message-content {
                display: block; padding: 1px 3px; margin: 0; border-radius: 3px;
            }
            .${CLASSIC_MODE_LIST_CLASS} .merged-message-wrapper > ${MESSAGE_BOX_SELECTOR} {}
        `;

        if (currentSettings.useAlternatingBackgrounds) {
            css += `
                .merged-message-wrapper .merged-message-content:nth-child(odd) {
                    background-color: rgba(${currentSettings.bgColor1_rgb}, ${currentSettings.bgColor1_alpha});
                }
                .merged-message-wrapper .merged-message-content:nth-child(even) {
                    background-color: rgba(${currentSettings.bgColor2_rgb}, ${currentSettings.bgColor2_alpha});
                }
            `;
        } else {
            // Ensure no background is applied if option is off
            css += `
                .merged-message-wrapper .merged-message-content:nth-child(odd),
                .merged-message-wrapper .merged-message-content:nth-child(even) {
                    background-color: transparent !important;
                }
            `;
        }

        // Styles for newline option
        css += `
            ${MESSAGE_BOX_SELECTOR} > span[class*="senderContainer___"] + .${NO_NEWLINE_FIRST_BUBBLE_AGGREGATOR_CLASS} {
                display: inline !important; margin-left: 0.3em;
            }
            .${NO_NEWLINE_FIRST_BUBBLE_AGGREGATOR_CLASS} > .merged-message-content:first-child {
                display: inline !important;
            }
            .${NO_NEWLINE_FIRST_BUBBLE_AGGREGATOR_CLASS} > .merged-message-content:not(:first-child) {
                display: block !important;
            }
            .${NO_NEWLINE_FIRST_CLASSIC_AGGREGATOR_CLASS} > .merged-message-content:first-child {
                display: inline !important; margin-left: 0.3em;
            }
        `;
        styleElement.innerHTML = css;
    }

    function toggleColorSettingsDisabledState(disabled) {
        const elementsToToggle = [];
        if (colorSetting1Div) elementsToToggle.push(colorSetting1Div);
        if (colorSetting2Div) elementsToToggle.push(colorSetting2Div);

        elementsToToggle.forEach(container => {
            container.style.opacity = disabled ? '0.5' : '1';
            const inputs = container.querySelectorAll('input');
            inputs.forEach(input => input.disabled = disabled);
        });
    }


    function createSettingsUI() {
        const settingsPanelContent = document.querySelector(SETTINGS_PANEL_SELECTOR);
        if (!settingsPanelContent) return;

        const protoCheckboxLabel = settingsPanelContent.querySelector('label[class*="subtitle___"][class^="root___"]:has(input[type="checkbox"])');
        const checkboxLabelClass = protoCheckboxLabel ? protoCheckboxLabel.className : 'abs-checkbox-label';

        const protoSectionTitle = settingsPanelContent.querySelector(`span[class^="header___"]`);
        const sectionTitleClass = protoSectionTitle ? protoSectionTitle.className : 'abs-section-title';

        const protoSettingsContainer = settingsPanelContent.querySelector(`div[class^="root___"]:has(span[class^="header___"])`);
        const settingsContainerClass = protoSettingsContainer ? protoSettingsContainer.className : 'abs-settings-block';

        const generalSettingsSliderContainer = settingsPanelContent.querySelector('div[class*="panelSizeContainer___"] > div[class^="root___"]');
        const sliderContainerClass = generalSettingsSliderContainer ? generalSettingsSliderContainer.className : 'abs-slider-container';

        const protoSliderLabel = generalSettingsSliderContainer ? generalSettingsSliderContainer.querySelector(`label[class^="label___"]`) : null;
        const sliderLabelClass = protoSliderLabel ? protoSliderLabel.className : 'abs-slider-label';

        const protoSliderInput = generalSettingsSliderContainer ? generalSettingsSliderContainer.querySelector(`input[type="range"][class^="range___"]`) : null;
        const sliderInputClass = protoSliderInput ? protoSliderInput.className : 'abs-slider-input';


        const separator = document.createElement('div');
        separator.style.height = '1px';
        separator.style.backgroundColor = 'var(--chat-divider-color, #444)';
        separator.style.margin = '15px 0';

        const sectionTitleElement = document.createElement('span');
        sectionTitleElement.className = sectionTitleClass;
        sectionTitleElement.textContent = 'Anti Bubble Spam Settings';
        sectionTitleElement.style.display = 'block';
        sectionTitleElement.style.marginBottom = '8px';

        const settingsContainerElement = document.createElement('div');
        settingsContainerElement.className = settingsContainerClass;
        settingsContainerElement.style.marginBottom = '15px';

        const enableLabel = document.createElement('label');
        enableLabel.className = checkboxLabelClass;
        enableLabel.style.display = 'flex';
        enableLabel.style.alignItems = 'center';
        enableLabel.style.marginBottom = '10px';

        const enableCheckbox = document.createElement('input');
        enableCheckbox.type = 'checkbox';
        enableCheckbox.checked = currentSettings.enabled;
        enableCheckbox.id = SCRIPT_ID_PREFIX + 'enable_script';
        enableCheckbox.style.marginRight = '8px';
        enableCheckbox.addEventListener('change', (e) => {
            currentSettings.enabled = e.target.checked;
            saveSettings();
            updateStyles(); // Update styles immediately
            processAllOpenChatLists(); // Re-process chats to apply/remove merging
        });
        enableLabel.appendChild(enableCheckbox);
        enableLabel.append(' Enable Anti Bubble Spam');
        settingsContainerElement.appendChild(enableLabel);

        const newLineLabel = document.createElement('label');
        newLineLabel.className = checkboxLabelClass;
        newLineLabel.style.display = 'flex';
        newLineLabel.style.alignItems = 'center';
        newLineLabel.style.marginBottom = '10px';

        const newLineCheckbox = document.createElement('input');
        newLineCheckbox.type = 'checkbox';
        newLineCheckbox.checked = currentSettings.startFirstMessageOnNewLine;
        newLineCheckbox.id = SCRIPT_ID_PREFIX + 'newLine_script';
        newLineCheckbox.style.marginRight = '8px';
        newLineCheckbox.addEventListener('change', (e) => {
            currentSettings.startFirstMessageOnNewLine = e.target.checked;
            saveSettings();
            updateStyles();
            processAllOpenChatLists();
        });
        newLineLabel.appendChild(newLineCheckbox);
        newLineLabel.append(' Start first message on new line');
        settingsContainerElement.appendChild(newLineLabel);

        const altBgLabel = document.createElement('label');
        altBgLabel.className = checkboxLabelClass;
        altBgLabel.style.display = 'flex';
        altBgLabel.style.alignItems = 'center';
        altBgLabel.style.marginBottom = '15px';

        const altBgCheckbox = document.createElement('input');
        altBgCheckbox.type = 'checkbox';
        altBgCheckbox.checked = currentSettings.useAlternatingBackgrounds;
        altBgCheckbox.id = SCRIPT_ID_PREFIX + 'altBg_script';
        altBgCheckbox.style.marginRight = '8px';
        altBgCheckbox.addEventListener('change', (e) => {
            currentSettings.useAlternatingBackgrounds = e.target.checked;
            saveSettings();
            updateStyles();
            toggleColorSettingsDisabledState(!currentSettings.useAlternatingBackgrounds);
        });
        altBgLabel.appendChild(altBgCheckbox);
        altBgLabel.append(' Use alternating background colors');
        settingsContainerElement.appendChild(altBgLabel);


        function updateSliderBackground(slider) {
            const value = (slider.value - slider.min) / (slider.max - slider.min) * 100;
            slider.style.background = `linear-gradient(to right, var(--chat-range-progress-color-default) ${value}%, var(--chat-range-range-color) ${value}%, var(--chat-range-range-color) 100%)`;
        }

        function createColorSetting(idPrefix, labelText) {
            const mainSettingDiv = document.createElement('div');
            mainSettingDiv.style.marginTop = '10px';
            mainSettingDiv.style.display = 'flex';
            mainSettingDiv.style.flexDirection = 'column';
            mainSettingDiv.style.gap = '8px';

            const colorRowDiv = document.createElement('div');
            colorRowDiv.style.display = 'flex';
            colorRowDiv.style.alignItems = 'center';

            const colorLabel = document.createElement('label');
            colorLabel.className = sliderLabelClass;
            colorLabel.textContent = labelText + ': ';
            colorLabel.style.minWidth = '160px';
            colorLabel.style.marginRight = '5px';

            const colorInput = document.createElement('input');
            colorInput.type = 'color';
            colorInput.value = rgbStringToHex(currentSettings[idPrefix + '_rgb']);
            colorInput.style.minWidth = '40px';
            colorInput.style.height = '25px';
            colorInput.style.padding = '0 2px';
            colorInput.style.border = '1px solid #555';
            colorInput.style.backgroundColor = '#333';
            colorInput.addEventListener('input', (e) => {
                const rgb = hexToRgbString(e.target.value);
                if (rgb) {
                    currentSettings[idPrefix + '_rgb'] = rgb;
                    saveSettings();
                    updateStyles();
                }
            });
            colorRowDiv.appendChild(colorLabel);
            colorRowDiv.appendChild(colorInput);

            const alphaSliderDiv = document.createElement('div');
            alphaSliderDiv.className = sliderContainerClass;

            const alphaLabelElement = document.createElement('label');
            alphaLabelElement.className = sliderLabelClass;
            const updateAlphaLabelText = () => {
                alphaLabelElement.textContent = `Alpha (${(currentSettings[idPrefix + '_alpha'] * 100).toFixed(0)}%)`;
            };
            updateAlphaLabelText();

            const alphaSlider = document.createElement('input');
            alphaSlider.type = 'range';
            alphaSlider.min = '0';
            alphaSlider.max = '1';
            alphaSlider.step = '0.01';
            alphaSlider.value = currentSettings[idPrefix + '_alpha'];
            alphaSlider.className = sliderInputClass;
            updateSliderBackground(alphaSlider);

            alphaSlider.addEventListener('input', (e) => {
                currentSettings[idPrefix + '_alpha'] = parseFloat(e.target.value);
                updateAlphaLabelText();
                updateSliderBackground(e.target);
                saveSettings();
                updateStyles();
            });

            alphaSliderDiv.appendChild(alphaLabelElement);
            alphaSliderDiv.appendChild(alphaSlider);

            mainSettingDiv.appendChild(colorRowDiv);
            mainSettingDiv.appendChild(alphaSliderDiv);
            return mainSettingDiv;
        }

        colorSetting1Div = createColorSetting('bgColor1', 'Background 1 (Odd)');
        colorSetting2Div = createColorSetting('bgColor2', 'Background 2 (Even)');
        settingsContainerElement.appendChild(colorSetting1Div);
        settingsContainerElement.appendChild(colorSetting2Div);

        toggleColorSettingsDisabledState(!currentSettings.useAlternatingBackgrounds);


        const messageStylingSection = Array.from(settingsPanelContent.children).find(child =>
            child.querySelector('span[class*="header___"]')?.textContent.includes('Message Styling Settings')
        );

        if (messageStylingSection) {
            settingsPanelContent.insertBefore(separator, messageStylingSection);
            settingsPanelContent.insertBefore(sectionTitleElement, messageStylingSection);
            settingsPanelContent.insertBefore(settingsContainerElement, messageStylingSection);
        } else {
            settingsPanelContent.appendChild(separator);
            settingsPanelContent.appendChild(sectionTitleElement);
            settingsContainerElement.appendChild(settingsContainerElement);
        }
    }

    function getSenderInfo(messageElement) {
        const classic = isClassicMode(messageElement);
        let isSelfMessage = false;

        if (classic) {
            if (messageElement.style.outlineColor === 'rgb(170, 0, 110)' || messageElement.style.outline.includes('rgb(170, 0, 110)')) {
                isSelfMessage = true;
            }
        } else {
            if (messageElement.className.includes(SELF_MESSAGE_CLASS_PART_BUBBLE)) {
                isSelfMessage = true;
            }
        }

        if (isSelfMessage) {
            return { id: 'self', nameElement: null, isSelf: true, classic: classic };
        }

        let senderLinkElement;
        let nameElementContainer;
        const boxElement = messageElement.querySelector(MESSAGE_BOX_SELECTOR);
        if (!boxElement) return { id: 'other_unknown_nobox_' + Date.now() + Math.random(), nameElement: null, isSelf: false, classic: classic };


        if (classic) {
            const senderSpan = boxElement.querySelector('span:first-child');
            senderLinkElement = senderSpan ? senderSpan.querySelector('a[href*="profiles.php?XID="]') : null;
            nameElementContainer = senderSpan ? senderSpan.cloneNode(true) : null;
        } else {
            senderLinkElement = boxElement.querySelector(SENDER_LINK_SELECTOR);
            nameElementContainer = boxElement.querySelector(SENDER_CONTAINER_SELECTOR_BUBBLE)?.cloneNode(true);
        }

        if (senderLinkElement) {
            const href = senderLinkElement.getAttribute('href');
            const match = href.match(/XID=(\d+)/);
            const senderId = match ? match[1] : senderLinkElement.textContent.trim().replace(':', '');
            return { id: senderId, nameElement: nameElementContainer, isSelf: false, classic: classic };
        }

        if (!classic) {
            const chatWindow = messageElement.closest('div[id^="private-"]');
            if (chatWindow) {
                return { id: `other_private_${chatWindow.id}`, nameElement: null, isSelf: false, classic: classic };
            }
        }
        return { id: 'other_unknown_' + Date.now() + Math.random(), nameElement: null, isSelf: false, classic: classic };
    }

    function storeOriginalHTMLIfNotExists(messageElement) {
        if (!messageElement.hasAttribute(DATA_ORIGINAL_HTML_ATTR)) {
            const boxElement = messageElement.querySelector(MESSAGE_BOX_SELECTOR);
            if (!boxElement) {
                 messageElement.setAttribute(DATA_ORIGINAL_HTML_ATTR, '');
                 return;
            }
            const messageTextParent = boxElement.querySelector(MESSAGE_TEXT_CONTAINER_SELECTOR);
            const rawHtml = messageTextParent ? messageTextParent.innerHTML : '';
            messageElement.setAttribute(DATA_ORIGINAL_HTML_ATTR, rawHtml);
        }
    }

    function getMessageContents(messageElement) {
        const originalHtml = messageElement.getAttribute(DATA_ORIGINAL_HTML_ATTR);
        if (originalHtml === null || originalHtml.trim() === '') return null;
        const contentSpan = document.createElement('span');
        contentSpan.className = 'merged-message-content';
        contentSpan.innerHTML = originalHtml;
        return contentSpan;
    }

    function mergeGroup(group) {
        // Unmerging logic: Restore original HTML for each message in the group
        if (!currentSettings.enabled || !group || group.length < 2) {
            group.forEach(msgNode => {
                msgNode.style.display = '';
                msgNode.classList.remove('merged-message-wrapper', 'self-merged', 'processed-by-antibubble');

                const boxElement = msgNode.querySelector(MESSAGE_BOX_SELECTOR);
                if (boxElement) {
                    const textAggregator = boxElement.querySelector(MESSAGE_TEXT_CONTAINER_SELECTOR);
                    if (textAggregator) {
                        textAggregator.classList.remove(NO_NEWLINE_FIRST_BUBBLE_AGGREGATOR_CLASS, NO_NEWLINE_FIRST_CLASSIC_AGGREGATOR_CLASS);
                        const originalHtml = msgNode.getAttribute(DATA_ORIGINAL_HTML_ATTR);
                        if (originalHtml !== null) { // Ensure attribute exists
                            textAggregator.innerHTML = originalHtml;
                        }
                    }
                }
            });
            return;
        }

        // Merging logic
        const firstMessageNode = group[0];
        const senderInfo = getSenderInfo(firstMessageNode);

        firstMessageNode.classList.add('merged-message-wrapper');
        if (senderInfo.isSelf) firstMessageNode.classList.add('self-merged');

        let messageTextAggregator;
        const boxElement = firstMessageNode.querySelector(MESSAGE_BOX_SELECTOR);
        if (!boxElement) return;


        if (senderInfo.classic) {
            messageTextAggregator = boxElement.querySelector(MESSAGE_TEXT_CONTAINER_SELECTOR);
            if (!messageTextAggregator) {
                messageTextAggregator = document.createElement('span');
                const bodySpan = boxElement.querySelector('span[class*="body___"]');
                messageTextAggregator.className = bodySpan ? bodySpan.className.replace(/body___[^ ]+/, 'message___') : 'message___';
                messageTextAggregator.classList.add('abs-classic-text-aggregator-fallback');
                boxElement.appendChild(messageTextAggregator);
            }
            messageTextAggregator.classList.remove(NO_NEWLINE_FIRST_BUBBLE_AGGREGATOR_CLASS);
            if (!currentSettings.startFirstMessageOnNewLine) {
                messageTextAggregator.classList.add(NO_NEWLINE_FIRST_CLASSIC_AGGREGATOR_CLASS);
            } else {
                messageTextAggregator.classList.remove(NO_NEWLINE_FIRST_CLASSIC_AGGREGATOR_CLASS);
            }
        } else {
            if (!senderInfo.isSelf && senderInfo.nameElement) {
                const existingSenderContainer = boxElement.querySelector(SENDER_CONTAINER_SELECTOR_BUBBLE);
                if (existingSenderContainer) existingSenderContainer.classList.add('merged-sender-name');
            }
            messageTextAggregator = boxElement.querySelector(MESSAGE_TEXT_CONTAINER_SELECTOR);
            if (!messageTextAggregator) {
                messageTextAggregator = document.createElement(senderInfo.isSelf ? 'p' : 'span');
                const bodyElement = boxElement.querySelector('p[class*="body___"], span[class*="body___"]');
                messageTextAggregator.className = bodyElement ? bodyElement.className.replace(/body___[^ ]+/, 'message___') : 'message___';
                messageTextAggregator.classList.add('abs-bubble-text-aggregator-fallback');
                boxElement.appendChild(messageTextAggregator);
            }
            messageTextAggregator.classList.remove(NO_NEWLINE_FIRST_CLASSIC_AGGREGATOR_CLASS);
            if (!currentSettings.startFirstMessageOnNewLine && !senderInfo.isSelf) {
                messageTextAggregator.classList.add(NO_NEWLINE_FIRST_BUBBLE_AGGREGATOR_CLASS);
            } else {
                messageTextAggregator.classList.remove(NO_NEWLINE_FIRST_BUBBLE_AGGREGATOR_CLASS);
            }
        }

        if (!messageTextAggregator) return;
        messageTextAggregator.innerHTML = '';

        group.forEach((msgNodeInGroup) => {
            storeOriginalHTMLIfNotExists(msgNodeInGroup);
            const contentSpan = getMessageContents(msgNodeInGroup);
            if (contentSpan) {
                messageTextAggregator.appendChild(contentSpan);
            }
        });

        for (let k = 0; k < group.length; k++) {
            if (k > 0) group[k].style.display = 'none';
            group[k].classList.add('processed-by-antibubble');
        }
    }

    function processMessagesInList(messageListElement) {
        // --- RESET PHASE ---
        Array.from(messageListElement.children).forEach(child => {
            if (child.matches(MESSAGE_ROOT_SELECTOR)) {
                child.style.display = '';
                child.classList.remove('merged-message-wrapper', 'self-merged', 'processed-by-antibubble');

                const boxElement = child.querySelector(MESSAGE_BOX_SELECTOR);
                if (boxElement) {
                    const textContainer = boxElement.querySelector(MESSAGE_TEXT_CONTAINER_SELECTOR);
                    if (textContainer) {
                        textContainer.classList.remove(NO_NEWLINE_FIRST_BUBBLE_AGGREGATOR_CLASS, NO_NEWLINE_FIRST_CLASSIC_AGGREGATOR_CLASS);
                        const originalHtml = child.getAttribute(DATA_ORIGINAL_HTML_ATTR);
                        if (originalHtml !== null) {
                            textContainer.innerHTML = originalHtml;
                        }
                    }
                }
            }
        });

        if (!currentSettings.enabled) {
            updateStyles();
            return;
        }
        // --- END RESET PHASE ---

        Array.from(messageListElement.children).forEach(child => {
            if (child.matches(MESSAGE_ROOT_SELECTOR)) storeOriginalHTMLIfNotExists(child);
        });

        const children = Array.from(messageListElement.children);
        let lastSenderId = null;
        let currentMessageGroup = [];

        for (let i = 0; i < children.length; i++) {
            const currentElement = children[i];
            if (currentElement.style.display === 'none' && currentElement.classList.contains('processed-by-antibubble')) continue;

            if (!currentElement.matches(MESSAGE_ROOT_SELECTOR) || currentElement.matches(TIMESTAMP_DIVIDER_SELECTOR)) {
                if (currentMessageGroup.length > 0) mergeGroup(currentMessageGroup);
                currentMessageGroup = []; lastSenderId = null; continue;
            }
            const senderInfo = getSenderInfo(currentElement);
            if (senderInfo.id && senderInfo.id === lastSenderId) {
                currentMessageGroup.push(currentElement);
            } else {
                if (currentMessageGroup.length > 0) mergeGroup(currentMessageGroup);
                currentMessageGroup = [currentElement]; lastSenderId = senderInfo.id;
            }
        }
        if (currentMessageGroup.length > 0) mergeGroup(currentMessageGroup);
        updateStyles();
    }

    function processAllOpenChatLists() {
        const chatRootElement = document.getElementById(CHAT_ROOT_ID);
        if (chatRootElement) {
            const allLists = chatRootElement.querySelectorAll(MESSAGE_LIST_SELECTOR);
            allLists.forEach(list => processMessagesInList(list));
        }
    }

    const observerCallback = (mutationsList) => {
        if (!currentSettings.enabled) return;
        let listsToReProcess = new Set();
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                mutation.addedNodes.forEach(addedNode => {
                    if (addedNode.nodeType === Node.ELEMENT_NODE) {
                        if (addedNode.matches(MESSAGE_ITEM_SELECTOR) || addedNode.querySelector(MESSAGE_LIST_SELECTOR)) {
                            const lists = addedNode.querySelectorAll(MESSAGE_LIST_SELECTOR);
                            lists.forEach(list => listsToReProcess.add(list));
                        } else if (addedNode.matches(MESSAGE_ROOT_SELECTOR)) {
                            const parentList = addedNode.closest(MESSAGE_LIST_SELECTOR);
                            if (parentList) listsToReProcess.add(parentList);
                        }
                    }
                });
            }
        }
        if (listsToReProcess.size > 0) {
            setTimeout(() => {
                listsToReProcess.forEach(list => {
                    processMessagesInList(list);
                    if (!list.dataset.antibubbleObserved) {
                        observeMessageList(list);
                        list.dataset.antibubbleObserved = 'true';
                    }
                });
            }, 200);
        }
    };

    function observeMessageList(listElement) {
        const listObserver = new MutationObserver(() => {
            if (!currentSettings.enabled) return;
            setTimeout(() => processMessagesInList(listElement), 200);
        });
        listObserver.observe(listElement, { childList: true });
    }

    function initialize() {
        loadSettings();
        updateStyles();

        const settingsPanelObserver = new MutationObserver(() => {
            const panel = document.querySelector(SETTINGS_PANEL_SELECTOR);
            if (panel) {
                if (!document.getElementById(SCRIPT_ID_PREFIX + 'enable_script')) {
                    createSettingsUI();
                }
            }
        });
        const chatRootForPanel = document.getElementById(CHAT_ROOT_ID) || document.body;
        settingsPanelObserver.observe(chatRootForPanel, { childList: true, subtree: true });

        const chatRootElement = document.getElementById(CHAT_ROOT_ID);
        if (!chatRootElement) {
            setTimeout(initialize, 1000); return;
        }

        processAllOpenChatLists();
        const existingMessageLists = chatRootElement.querySelectorAll(MESSAGE_LIST_SELECTOR);
        existingMessageLists.forEach(list => {
            if (!list.dataset.antibubbleObserved) {
                observeMessageList(list);
                list.dataset.antibubbleObserved = 'true';
            }
        });
        const mainObserver = new MutationObserver(observerCallback);
        mainObserver.observe(chatRootElement, { childList: true, subtree: true });
    }

    if (document.readyState === "complete" || document.readyState === "interactive") {
        initialize();
    } else {
        window.addEventListener('DOMContentLoaded', initialize);
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址