Dreadcast Dynamic Messages V1 logs

Messagerie dynamique

As of 2025-04-07. See the latest version.

// ==UserScript==
// @name           Dreadcast Dynamic Messages V1 logs
// @namespace      http://tampermonkey.net/
// @version        0.9.3
// @description    Messagerie dynamique
// @author         Laïn
// @match          https://www.dreadcast.net/Main*
// @grant          GM_addStyle
// @grant          GM_xmlhttpRequest
// @grant          unsafeWindow
// @grant          GM_info
// ==/UserScript==


(function() {
    'use strict';


    // --- Global Variables & Constants ---
    let MY_NAME = null;
    const ACTIVE_CONVERSATIONS = {}; // Stores { customWindow, originalWindow, latestMessageId, oldestMessageId, allMessagesLoaded, isLoadingOlder, participants, hasUnreadNotification }
    const INITIAL_LOAD_COUNT = 20;
    const LOAD_MORE_COUNT = 25;
    // Click simulation delays
    const REFIND_DELAY = 50; // Wait after first click before re-find in double-click
    const UI_CLICK_DELAY = 50; // Short delay *before* simulated UI navigation clicks
    const UI_WAIT_DELAY = 100; // Wait *after* UI navigation clicks/style changes for potential animation/update
    const WAIT_FOR_ELEMENT_TIMEOUT = 1500; // Timeout for waitForElement
    const NOTIFICATION_SOUND_URL = 'https://opengameart.org/sites/default/files/audio_preview/GUI%20Sound%20Effects_031.mp3.ogg'; // Sound for new messages IN OPEN windows
    const UNOPENED_NOTIFICATION_SOUND_URL = 'https://orangefreesounds.com/wp-content/uploads/2020/10/Simple-notification-alert.mp3'; // <<< NEW: Sound for new messages in UNOPENED windows


    // --- Version Info ---
    // Using a fixed string as GM_info might cause issues if script is copy-pasted without manager
    const SCRIPT_VERSION = '0.9.3-unopened-sound'; // Updated version number


    // --- UI Constants ---
    const MIN_WINDOW_WIDTH = 300;
    const MIN_WINDOW_HEIGHT = 200;
    const DEFAULT_THEME_COLOR = '#0b5a9c';
    const GLOBAL_THEME_STORAGE_KEY = 'dmm_theme_color_v1';
    const CONVERSATION_COLORS_STORAGE_KEY = 'dmm_conversation_colors_v2';
    // Notification Colors
    const UNREAD_NOTIFICATION_COLOR = '#cca300'; // Golden Yellow (less bright) for unread header
    const UNREAD_TEXT_COLOR = '#101010'; // Darker text for contrast on yellow
    const UNREAD_BORDER_COLOR = '#b0891a'; // Slightly darker gold border


    // --- Observers ---
    let mainObserver = null; // Observes body for added original message windows


    // --- Utility Functions ---
    function getMyCharacterName() {
        const nameElement = document.getElementById('txt_pseudo');
        if (nameElement) return nameElement.textContent.trim();
        console.error("DMM: #txt_pseudo not found?");
        return null;
    }

    function waitForElement(selector, timeout = WAIT_FOR_ELEMENT_TIMEOUT, container = document) {
        return new Promise((resolve, reject) => {
            const startTime = Date.now();
            const interval = setInterval(() => {
                try {
                    const element = container.querySelector(selector);
                    // Check visibility more robustly
                    if (element && document.body.contains(element) && element.offsetParent !== null && getComputedStyle(element).visibility !== 'hidden' && getComputedStyle(element).display !== 'none') {
                        clearInterval(interval);
                        resolve(element);
                    } else if (Date.now() - startTime > timeout) {
                        clearInterval(interval);
                        reject(new Error(`Element ${selector} not found or not visible within ${timeout}ms`));
                    }
                } catch (e) {
                    clearInterval(interval);
                    console.error(`DMM waitForElement: Error during querySelector for "${selector}"`, e);
                    reject(new Error(`Error finding element ${selector}: ${e.message}`));
                }
            }, 50);
        });
    }


    // --- Theme Color Management ---
    function getSavedGlobalThemeColor() {
        return localStorage.getItem(GLOBAL_THEME_STORAGE_KEY) || DEFAULT_THEME_COLOR;
    }
    function getConversationColors() {
        try {
            const stored = localStorage.getItem(CONVERSATION_COLORS_STORAGE_KEY);
            return stored ? JSON.parse(stored) : {};
        } catch (e) {
            console.error("DMM: Failed to parse conversation colors from localStorage.", e);
            return {};
        }
    }
    function saveConversationColors(allColors) {
        try {
            localStorage.setItem(CONVERSATION_COLORS_STORAGE_KEY, JSON.stringify(allColors));
        } catch (e) {
            console.error("DMM: Failed to save conversation colors to localStorage.", e);
        }
    }
    function getConversationSetting(conversationId) {
        const allColors = getConversationColors();
        const setting = allColors[conversationId];
        if (setting && typeof setting === 'object' && typeof setting.enabled === 'boolean' && typeof setting.color === 'string') {
            return setting;
        }
        return { enabled: false, color: DEFAULT_THEME_COLOR };
    }
    function setConversationSetting(conversationId, setting) {
        if (typeof setting !== 'object' || typeof setting.enabled !== 'boolean' || typeof setting.color !== 'string') {
            console.warn(`DMM: Invalid setting provided for conversation ${conversationId}:`, setting);
            return;
        }
        const allColors = getConversationColors();
        allColors[conversationId] = setting;
        saveConversationColors(allColors);
    }
    function applyCurrentTheme(chatWindow, conversationId) {
        if (!chatWindow || !conversationId) return;
        const specificSetting = getConversationSetting(conversationId);
        const globalColor = getSavedGlobalThemeColor();
        const effectiveColor = specificSetting.enabled ? specificSetting.color : globalColor;

        // Apply standard theme colors
        chatWindow.style.setProperty('--dmm-primary-color', effectiveColor);
        chatWindow.style.setProperty('--dmm-header-bg', `color-mix(in srgb, ${effectiveColor} 70%, #080808)`);
        chatWindow.style.setProperty('--dmm-button-hover-bg', `color-mix(in srgb, ${effectiveColor} 85%, #ffffff)`);
        chatWindow.style.setProperty('--dmm-bubble-timestamp', `color-mix(in srgb, ${effectiveColor} 40%, #ffffff)`);
        chatWindow.style.setProperty('--dmm-border-color', effectiveColor);
        chatWindow.style.setProperty('--dmm-menu-hover-bg', effectiveColor);
        chatWindow.style.setProperty('--dmm-resize-border', `color-mix(in srgb, ${effectiveColor} 50%, #667788)`);

        // Re-apply notification style if needed (theme change shouldn't clear it)
        const convData = ACTIVE_CONVERSATIONS[conversationId];
        if (convData?.hasUnreadNotification) {
             chatWindow.classList.add('has-unread-notification');
        } else {
             chatWindow.classList.remove('has-unread-notification');
        }
    }
    function saveGlobalThemeColor(newColor) {
        localStorage.setItem(GLOBAL_THEME_STORAGE_KEY, newColor);
        for (const [convId, convData] of Object.entries(ACTIVE_CONVERSATIONS)) {
            if (convData.customWindow && document.body.contains(convData.customWindow)) {
                applyCurrentTheme(convData.customWindow, convId);
            }
        }
    }


    // --- Styles ---
     function addChatStyles() {
         GM_addStyle(`
             /* Styles pour la fenêtre de chat personnalisée */
             .custom-chat-window {
                 --dmm-primary-color: ${DEFAULT_THEME_COLOR}; /* Fallback */
                 --dmm-header-bg: color-mix(in srgb, var(--dmm-primary-color) 70%, #080808);
                 --dmm-button-hover-bg: color-mix(in srgb, var(--dmm-primary-color) 85%, #ffffff);
                 --dmm-bubble-timestamp: color-mix(in srgb, var(--dmm-primary-color) 40%, #ffffff);
                 --dmm-border-color: var(--dmm-primary-color);
                 --dmm-menu-hover-bg: var(--dmm-primary-color);
                 --dmm-resize-border: color-mix(in srgb, var(--dmm-primary-color) 50%, #667788);
                 position: fixed; z-index: 999999; width: 450px; height: 600px;
                 max-height: 95vh; max-width: 95vw; min-width: ${MIN_WINDOW_WIDTH}px; min-height: ${MIN_WINDOW_HEIGHT}px;
                 border: 1px solid var(--dmm-border-color); background-color: #101010; color: #e0e0e0;
                 border-radius: 5px; box-shadow: 0 0 15px color-mix(in srgb, var(--dmm-primary-color) 30%, transparent);
                 display: flex; flex-direction: column; top: 100px; left: calc(50% - 225px);
                 overflow: hidden; transition: max-height 0.3s ease-out, border-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out; /* Added transitions */
             }
             .custom-chat-head {
                 background-color: var(--dmm-header-bg); color: #fff; padding: 6px 10px; font-weight: bold;
                 border-bottom: 1px solid var(--dmm-border-color); cursor: move; display: flex;
                 justify-content: space-between; align-items: center; border-radius: 5px 5px 0 0;
                 flex-shrink: 0; position: relative;
                 transition: background-color 0.3s ease-in-out, color 0.3s ease-in-out, border-bottom-color 0.3s ease-in-out; /* Add transition for smooth color change */
             }
             /* Style for Unread Notification */
             .custom-chat-window.has-unread-notification {
                border-color: ${UNREAD_BORDER_COLOR}; /* Change main border */
                box-shadow: 0 0 15px color-mix(in srgb, ${UNREAD_NOTIFICATION_COLOR} 40%, transparent); /* Change shadow color */
             }
             .custom-chat-window.has-unread-notification .custom-chat-head {
                 background-color: ${UNREAD_NOTIFICATION_COLOR};
                 color: ${UNREAD_TEXT_COLOR};
                 border-bottom-color: ${UNREAD_BORDER_COLOR};
             }
             .custom-chat-window.has-unread-notification .custom-chat-head .title {
                 color: ${UNREAD_TEXT_COLOR}; /* Ensure title text is dark */
             }
             .custom-chat-window.has-unread-notification .custom-chat-head .controls span {
                 color: #333; /* Darker control icons on yellow */
             }
             .custom-chat-window.has-unread-notification .custom-chat-head .controls span:hover {
                 color: #000; /* Black on hover */
             }
             /* End Unread Notification */

             .custom-chat-head .title { flex-grow: 1; padding-right: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
             .custom-chat-head .controls { display: flex; align-items: center; flex-shrink: 0; white-space: nowrap; }
             .custom-chat-head .controls span { cursor: pointer; padding: 0 5px; font-size: 1.2em; font-family: Arial, sans-serif; font-weight: bold; user-select: none; line-height: 1; transition: color 0.2s ease; }
             .custom-chat-head .controls span:hover { color: #ffdddd; }
             .custom-chat-content { flex-grow: 1; overflow-y: auto; padding: 10px; background-color: #1a1a1a; display: flex; flex-direction: column; gap: 5px; transition: opacity 0.2s ease-out; }
             .custom-chat-content.loading::after { content: "Chargement des messages..."; display: block; text-align: center; padding: 20px; color: #ccc; font-style: italic; }
             .load-more-container { text-align: center; padding: 8px; border-bottom: 1px solid #2a2a2a; margin-bottom: 5px; flex-shrink: 0; }
             .load-more-link { color: #87ceeb; cursor: pointer; text-decoration: underline; font-size: 0.9em; }
             .load-more-link:hover { color: #aaeebb; }
             .load-more-link.loading { color: #aaa; cursor: default; text-decoration: none; }
             .load-more-link.loading::before { content: "Chargement... "; }
             .chat-bubble { max-width: 80%; padding: 8px 14px; border-radius: 18px; margin-bottom: 4px; line-height: 1.4; word-wrap: break-word; position: relative; }
             .bubble-content { white-space: pre-wrap; }
             .my-bubble { background-color: var(--dmm-primary-color); color: #ffffff; align-self: flex-end; border-bottom-right-radius: 5px; transition: background-color 0.3s ease-in-out; }
             .their-bubble { background-color: #3a3a3a; color: #e0e0e0; align-self: flex-start; border-bottom-left-radius: 5px; }
             .bubble-sender-name { font-size: 0.8em; font-weight: bold; color: #87ceeb; margin-bottom: 3px; }
             .bubble-timestamp { font-size: 0.7em; color: #a0a0a0; display: block; text-align: right; margin-top: 4px; clear: both; }
             .my-bubble .bubble-timestamp { color: var(--dmm-bubble-timestamp); transition: color 0.3s ease-in-out;}
             .custom-chat-reply { padding: 8px; border-top: 1px solid var(--dmm-border-color); background-color: #101010; display: flex; gap: 5px; flex-shrink: 0; transition: opacity 0.2s ease-out, border-color 0.3s ease-in-out; }
             .custom-chat-reply textarea { flex-grow: 1; height: 50px; min-height: 30px; max-height: 150px; padding: 5px; border: 1px solid #333; background-color: #222; color: #ddd; resize: vertical; font-family: inherit; font-size: 0.9em; }
             .custom-chat-reply button { padding: 10px 15px; background-color: var(--dmm-primary-color); color: white; border: none; border-radius: 3px; cursor: pointer; font-weight: bold; align-self: center; transition: background-color 0.2s ease; }
             .custom-chat-reply button:hover { background-color: var(--dmm-button-hover-bg); }
             .custom-chat-reply button:disabled { background-color: #555; cursor: not-allowed; }
             .hidden-original-databox { position: absolute !important; top: -9999px !important; left: -9999px !important; opacity: 0 !important; pointer-events: none !important; z-index: -1 !important; width: 1px !important; height: 1px !important; overflow: hidden !important; }
             .custom-chat-window.collapsed { max-height: 35px !important; min-height: 35px !important; height: 35px !important; }
             .custom-chat-window.collapsed .custom-chat-content,
             .custom-chat-window.collapsed .custom-chat-reply,
             .custom-chat-window.collapsed .participants-panel,
             .custom-chat-window.collapsed .color-picker-panel,
             .custom-chat-window.collapsed .more-opts-menu,
             .custom-chat-window.collapsed .resize-handle { display: none; }
             .custom-chat-window.collapsed .custom-chat-head { border-radius: 5px; }
             .custom-chat-head .controls .theme-btn { padding: 0 8px; font-size: 1.1em; margin-right: 5px; font-family: 'Segoe UI Symbol', Arial, sans-serif; }
             .custom-chat-head .controls .participants-btn { padding: 0 8px; font-size: 1.2em; margin-right: 5px; font-family: 'Segoe UI Symbol', Arial, sans-serif; }
             .custom-chat-head .controls .more-opts-btn { padding: 0 8px; font-size: 1.4em; font-weight: bold; margin-right: 5px; font-family: 'Segoe UI Symbol', Arial, sans-serif; }
             .custom-chat-window .more-opts-menu { position: absolute; top: 35px; right: 5px; background-color: #2a2a2a; border: 1px solid var(--dmm-border-color); border-radius: 4px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); z-index: 1000001; min-width: 150px; padding: 5px 0; display: none; transition: border-color 0.3s ease-in-out; }
             .custom-chat-window .color-picker-panel { position: absolute; top: 35px; right: 5px; background-color: #2a2a2a; border: 1px solid var(--dmm-border-color); border-radius: 4px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); z-index: 1000001; padding: 10px; display: none; text-align: center; transition: border-color 0.3s ease-in-out; }
             .custom-chat-window .color-picker-panel label { display: block; margin-bottom: 5px; font-size: 0.9em; color: #ccc; }
             .custom-chat-window .color-picker-panel input[type="color"] { cursor: pointer; border: 1px solid #555; width: 50px; height: 30px; padding: 0; background-color: #333; }
             .custom-chat-window .more-opts-menu .menu-item { padding: 8px 15px; color: #e0e0e0; cursor: pointer; font-size: 0.9em; white-space: nowrap; }
             .custom-chat-window .more-opts-menu .menu-item:hover { background-color: var(--dmm-menu-hover-bg); color: #ffffff; }
             .custom-chat-window .more-opts-menu hr { border: none; border-top: 1px solid #444; margin: 5px 0; }
             .custom-chat-window .more-opts-menu .convo-settings-item { display: flex; align-items: center; justify-content: space-between; padding: 6px 15px; font-size: 0.9em; color: #ccc; }
             .custom-chat-window .more-opts-menu .convo-settings-item label { margin-right: 8px; white-space: nowrap; cursor: pointer; }
             .custom-chat-window .more-opts-menu .convo-settings-item input[type="checkbox"] { margin-right: 5px; cursor: pointer; vertical-align: middle; }
             .custom-chat-window .more-opts-menu .convo-settings-item input[type="color"] { cursor: pointer; border: 1px solid #555; width: 35px; height: 20px; padding: 0; background-color: #333; vertical-align: middle; }
             .custom-chat-window .more-opts-menu .convo-settings-item input[type="color"]:disabled { cursor: not-allowed; opacity: 0.5; }
             .participants-panel { position: absolute; top: 0; right: -250px; width: 250px; height: 100%; background-color: rgba(31, 31, 31, 0.95); border-left: 1px solid var(--dmm-border-color); box-shadow: -2px 0 5px rgba(0, 0, 0, 0.4); z-index: 999998; display: flex; flex-direction: column; transition: right 0.3s ease-in-out, border-color 0.3s ease-in-out; overflow: hidden; }
             .participants-panel.active { right: 0; }
             .participants-panel-header { padding: 8px 12px; font-weight: bold; color: #fff; background-color: var(--dmm-header-bg); border-bottom: 1px solid var(--dmm-border-color); flex-shrink: 0; display: flex; justify-content: space-between; align-items: center; transition: background-color 0.3s ease-in-out, border-color 0.3s ease-in-out; }
             .participants-panel-header .close-panel-btn { font-size: 1.2em; cursor: pointer; padding: 0 5px; }
             .participants-panel-list { padding: 10px; overflow-y: auto; flex-grow: 1; color: #ccc; font-size: 0.9em; line-height: 1.4; }
             .participants-panel-list p { margin: 0; padding: 0; word-break: break-word; }
             .resize-handle { position: absolute; bottom: 0; right: 0; width: 15px; height: 15px; background-color: transparent; border-bottom: 2px solid var(--dmm-resize-border); border-right: 2px solid var(--dmm-resize-border); cursor: nwse-resize; z-index: 1000000; transition: border-color 0.3s ease-in-out; }
             .custom-chat-window.has-unread-notification .resize-handle { /* Optional: Change resize handle color too */
                 border-color: color-mix(in srgb, ${UNREAD_BORDER_COLOR} 50%, #667788);
             }
         `);
     }


    // --- Dragging/Resizing ---
    function makeDraggable(element) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        const h = element.querySelector(".custom-chat-head");
        if (h) { h.onmousedown = dragMouseDown; }
        else { element.onmousedown = dragMouseDown; }
        function dragMouseDown(e) {
            // Prevent drag if clicking on controls, menus, panels, or resize handle
            if (e.target.closest('.controls span') || e.target.closest('.more-opts-menu') || e.target.closest('.participants-panel') || e.target.closest('.color-picker-panel') || e.target.classList.contains('resize-handle')) return;
            e = e || window.event;
            pos3 = e.clientX; pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        }
        function elementDrag(e) {
            e = e || window.event;
            e.preventDefault();
            pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY;
            pos3 = e.clientX; pos4 = e.clientY;
            element.style.top = Math.max(0, (element.offsetTop - pos2)) + "px";
            element.style.left = (element.offsetLeft - pos1) + "px";
        }
        function closeDragElement() {
            document.onmouseup = null; document.onmousemove = null;
        }
    }
    function makeResizable(element, handle) {
        let startX, startY, startWidth, startHeight;
        handle.addEventListener('mousedown', function(e) {
            e.preventDefault();
            startX = e.clientX; startY = e.clientY;
            const computedStyle = document.defaultView.getComputedStyle(element);
            startWidth = parseInt(computedStyle.width, 10);
            startHeight = parseInt(computedStyle.height, 10);
            document.addEventListener('mousemove', doDrag, false);
            document.addEventListener('mouseup', stopDrag, false);
        }, false);
        function doDrag(e) {
            let newWidth = startWidth + e.clientX - startX;
            let newHeight = startHeight + e.clientY - startY;
            newWidth = Math.max(MIN_WINDOW_WIDTH, newWidth);
            newHeight = Math.max(MIN_WINDOW_HEIGHT, newHeight);
            newWidth = Math.min(window.innerWidth - element.offsetLeft - 5, newWidth); // Prevent dragging off-screen right
            newHeight = Math.min(window.innerHeight - element.offsetTop - 5, newHeight); // Prevent dragging off-screen bottom
            element.style.width = newWidth + 'px';
            element.style.height = newHeight + 'px';
        }
        function stopDrag() {
            document.removeEventListener('mousemove', doDrag, false);
            document.removeEventListener('mouseup', stopDrag, false);
        }
    }


    // --- Message Parsing and Fetching ---
    function parseMessageElement(element) {
        const id = element.id.replace('convers_', '');
        const timestamp = element.querySelector('.ligne1')?.textContent.trim();
        const senderLine = element.querySelector('.ligne2')?.textContent.trim();
        const senderMatch = senderLine?.match(/Message de (.*)/);
        const sender = senderMatch ? senderMatch[1] : '?';
        if (!id || !timestamp || !senderLine) {
            // console.warn("DMM parseMessageElement: Failed to parse element", element);
            return null;
        }
        return { id, timestamp, sender };
    }
    function fetchMessageContent(messageId, conversationId, callback) {
        GM_xmlhttpRequest({
            method: "GET",
            url: `https://www.dreadcast.net/Menu/Messaging/action=ReadMessage&id_message=${messageId}&id_conversation=${conversationId}`,
            timeout: 15000,
            onload: function(r) {
                if (r.status === 200 && r.responseText) {
                    try {
                        let p = new DOMParser(), x = p.parseFromString(r.responseText, "text/xml"), m = x.querySelector("message");
                        const content = m ? (m.textContent || m.innerHTML).trim() : "Erreur: Contenu message vide";
                        callback(content);
                    } catch (e) { console.error(`%cDMM fetchMessageContent[${conversationId}]: Parse XML error for msg ${messageId}`, "color: red", e); callback("Erreur: Parse XML"); }
                } else { console.warn(`%cDMM fetchMessageContent[${conversationId}]: Load error for msg ${messageId}, Status: ${r.status}`, "color: red"); callback(`Erreur: Load ${r.status}`); }
            },
            onerror: function(e) { console.error(`%cDMM fetchMessageContent[${conversationId}]: Network error for msg ${messageId}`, "color: red", e); callback("Erreur: Réseau"); },
            ontimeout: function() { console.warn(`%cDMM fetchMessageContent[${conversationId}]: Timeout for msg ${messageId}`, "color: red"); callback("Erreur: Timeout"); }
        });
    }
    async function parseAndFetchInitialMessages(originalWindow, conversationId) {
        const logPrefix = `DMM PFIM (${conversationId}):`;
        const defaultResult = { messages: [], participants: [], totalMessages: 0, latestId: null, oldestId: null, allLoaded: true };
        const conversationZone = originalWindow.querySelector('.zone_conversation');
        if (!conversationZone) { console.warn(`${logPrefix} No .zone_conversation found.`); return defaultResult; }
        let participants = [];
        try {
            const participantsTitleDiv = Array.from(conversationZone.querySelectorAll('div')).find(div => div.textContent.includes('Participants'));
            if (participantsTitleDiv) {
                let currentElement = participantsTitleDiv.nextElementSibling;
                let participantsString = '';
                // Find the <p> tag containing participant names, skipping over potential <br> or other tags.
                while(currentElement && currentElement.tagName !== 'P') {
                    currentElement = currentElement.nextElementSibling;
                }
                 // Check if we found the P tag and make sure it doesn't contain a message link (edge case)
                 if (currentElement && currentElement.tagName === 'P' && !currentElement.querySelector('.link.conversation')) {
                    participantsString = currentElement.textContent.trim();
                }
                if (participantsString) { participants = participantsString.split(',').map(name => name.trim()).filter(name => name.length > 0); }
                // else { console.warn(`${logPrefix} Participants list <p> tag not found or empty near title.`); }
            } // else { console.warn(`${logPrefix} Participants title div not found.`); }
        } catch (e) { console.warn(`${logPrefix} Failed to parse participants list`, e); }


        const allElements = Array.from(conversationZone.querySelectorAll('.link.conversation'));
        const total = allElements.length;
        if (total === 0) { defaultResult.participants = participants; return defaultResult; }
        const fetchElements = allElements.slice(0, INITIAL_LOAD_COUNT);
        let fetchedData = [];
        let fetchPromises = [];
        for (const el of fetchElements) {
            const parsed = parseMessageElement(el);
            if (parsed) {
                let msgData = { ...parsed, content: null };
                fetchedData.push(msgData);
                fetchPromises.push(new Promise((resolve) => {
                    fetchMessageContent(msgData.id, conversationId, (content) => {
                        const targetMsg = fetchedData.find(m => m.id === msgData.id);
                        if (targetMsg) targetMsg.content = content;
                        resolve();
                    });
                }));
            }
        }
        try {
            if (fetchPromises.length > 0) await Promise.all(fetchPromises);
            fetchedData.reverse();
            const latestId = fetchedData.length > 0 ? fetchedData[fetchedData.length - 1].id : null;
            const oldestId = fetchedData.length > 0 ? fetchedData[0].id : null;
            const allLoaded = total <= INITIAL_LOAD_COUNT;
            const result = { messages: fetchedData, participants: participants, totalMessages: total, latestId: latestId, oldestId: oldestId, allLoaded: allLoaded };
            return result;
        } catch (error) {
            console.error(`${logPrefix} Error during Promise.all or processing:`, error);
            defaultResult.participants = participants;
            return defaultResult;
        }
    }


    // --- UI Building and Manipulation ---

    /**
     * Adds a message bubble to the chat window.
     * @param {object} msgData - The message data { id, timestamp, sender, content }.
     * @param {HTMLElement} container - The DOM element to add the bubble to (.custom-chat-content).
     * @param {string} conversationId - The ID of the conversation.
     * @param {boolean} [prepend=false] - True if the bubble should be added to the top (loading older).
     * @param {boolean} [isInitialLoad=false] - True if this bubble is being added during the initial window build.
     * @returns {string|null} The ID of the added message, or null if not added.
     */
    function addBubble(msgData, container, conversationId, prepend = false, isInitialLoad = false) {
        if (!MY_NAME || !container) {
            console.warn(`DMM addBubble: MY_NAME (${MY_NAME}) or container invalid. Cannot add bubble.`, msgData);
            return null;
        }
        const isMine = msgData.sender === MY_NAME;


        if (msgData.id) {
            const existingById = container.querySelector(`.chat-bubble[data-message-id="${msgData.id}"]`);
            if (existingById) {
                // console.log(`%cDMM addBubble: Preventing duplicate add for ID ${msgData.id}`, "color: gray");
                return null; // Return null to indicate it wasn't added
            }
        }

        const bubble = document.createElement('div');
        bubble.classList.add('chat-bubble');
        if (msgData.id) bubble.dataset.messageId = msgData.id;
        bubble.classList.add(isMine ? 'my-bubble' : 'their-bubble');

        if (!isMine) {
            const n = document.createElement('div');
            n.classList.add('bubble-sender-name');
            n.textContent = msgData.sender;
            bubble.appendChild(n);
        }

        const c = document.createElement('div');
        c.classList.add('bubble-content');
        const tempDiv = document.createElement('div');
        tempDiv.textContent = msgData.content || "..."; // Set text content safely
        c.innerHTML = tempDiv.innerHTML.replace(/\n/g, '<br>'); // Convert newlines to <br>
        bubble.appendChild(c);

        if (msgData.timestamp) {
            const t = document.createElement('span');
            t.classList.add('bubble-timestamp');
            t.textContent = msgData.timestamp;
            bubble.appendChild(t);
        }

        const isScrolledToBottom = Math.abs(container.scrollHeight - container.clientHeight - container.scrollTop) < 50;
        const shouldScrollDown = !prepend && (isScrolledToBottom || isMine);

        if (prepend) {
            const loadMoreElem = container.querySelector('.load-more-container');
            if (loadMoreElem) {
                loadMoreElem.insertAdjacentElement('afterend', bubble);
            } else {
                container.insertBefore(bubble, container.firstChild);
            }
        } else {
            container.appendChild(bubble);
        }

        // --- Play Sound & Set Notification State ---
        // Play sound AND set notification state only for incoming messages that are appended (not prepended) and not part of the initial load.
        if (!isMine && !prepend && !isInitialLoad) {
            // Sound (Only play the specific 'open window' sound here)
            try {
                const audio = new Audio(NOTIFICATION_SOUND_URL); // Use the standard notification sound
                audio.play().catch(e => {
                    console.warn("DMM: Open window audio playback failed (interaction might be required for sound):", e.name, e.message);
                });
            } catch (e) {
                console.error("DMM: Error creating or playing open window notification sound:", e);
            }

            // Visual Notification State
            const convData = ACTIVE_CONVERSATIONS[conversationId];
            const chatWindow = container.closest('.custom-chat-window'); // Find the parent window
            if (convData && chatWindow && !chatWindow.classList.contains('has-unread-notification')) { // Check if window exists and notification isn't already set
                convData.hasUnreadNotification = true;
                chatWindow.classList.add('has-unread-notification');
                // console.log(`%cDMM: Set unread notification for ${conversationId}`, "color: orange"); // Optional debug log
            }
        }
        // --- End Play Sound & Set Notification State ---


        if (shouldScrollDown) {
            requestAnimationFrame(() => {
                if (container && container.isConnected) {
                    container.scrollTop = container.scrollHeight;
                }
            });
        }
        return msgData.id; // Return the ID of the added bubble
    } // --- END of addBubble function ---


    function buildInitialChatUI(messages, container, conversationId, allMessagesLoaded) {
        container.innerHTML = '';
        container.classList.remove('loading');
        if (!MY_NAME) {
            container.innerHTML = "<p style='color:red;'>Erreur: Nom utilisateur non trouvé.</p>";
            return { latestId: null, oldestId: null };
        }
        if (!allMessagesLoaded) {
            addLoadMoreLink(container, conversationId);
        }
        let firstId = null;
        let lastId = null;
        messages.forEach(msg => {
            // Pass 'true' for the new isInitialLoad parameter
            const addedId = addBubble(msg, container, conversationId, false, true); // isInitialLoad=true here
            if (addedId) {
                if (!firstId || (parseInt(addedId) < parseInt(firstId))) { firstId = addedId; }
                if (!lastId || (parseInt(addedId) > parseInt(lastId))) { lastId = addedId; }
            }
        });
        // Scroll to bottom after initial build
        setTimeout(() => { if (container && container.isConnected) container.scrollTop = container.scrollHeight; }, 100);
        return { latestId: lastId, oldestId: firstId };
    }


    function addLoadMoreLink(container, conversationId) {
        if (container.querySelector('.load-more-container')) return; // Avoid adding multiple links
        const loadMoreContainer = document.createElement('div');
        loadMoreContainer.classList.add('load-more-container');
        const loadMoreLink = document.createElement('a');
        loadMoreLink.classList.add('load-more-link');
        loadMoreLink.textContent = 'Afficher les messages précédents';
        loadMoreLink.href = '#';
        loadMoreLink.onclick = (e) => {
            e.preventDefault();
            const cData = ACTIVE_CONVERSATIONS[conversationId];
            if (cData && !cData.isLoadingOlder) {
                loadOlderMessages(conversationId, loadMoreLink);
            }
        };
        loadMoreContainer.appendChild(loadMoreLink);
        container.insertBefore(loadMoreContainer, container.firstChild);
    }


    async function loadOlderMessages(conversationId, linkElement) {
        const cData = ACTIVE_CONVERSATIONS[conversationId];
        const container = cData?.customWindow?.querySelector('.custom-chat-content');
        if (!cData || !container || cData.isLoadingOlder || cData.allMessagesLoaded) {
            if (cData?.allMessagesLoaded && linkElement?.parentElement) linkElement.parentElement.remove(); // Clean up link if already loaded
            return;
        }
        cData.isLoadingOlder = true;
        linkElement.classList.add('loading');
        linkElement.textContent = ''; // Clear text while loading


        try {
             // Fetch the full conversation page HTML to get all message elements
             const response = await new Promise((resolve, reject) => {
                 GM_xmlhttpRequest({
                     method: "GET", url: `https://www.dreadcast.net/Menu/Messaging/action=OpenMessage&id_conversation=${conversationId}`, timeout: 15000,
                     onload: (res) => { if (res.status === 200 && res.responseText) resolve(res.responseText); else reject(`HTTP Status ${res.status}`); },
                     onerror: (err) => reject("Network Error"), ontimeout: () => reject("Timeout")
                 });
             });

             const parser = new DOMParser();
             const doc = parser.parseFromString(response, 'text/html');
             const messageList = doc.querySelector('.zone_conversation');
             if (!messageList) throw new Error("Could not find .zone_conversation in older messages response.");

             const allElements = Array.from(messageList.querySelectorAll('.link.conversation'));
             const currentOldestId = cData.oldestMessageId;
             let olderElementsToLoad = [];

             const currentOldestIndex = allElements.findIndex(el => el.id.replace('convers_', '') === currentOldestId);

             if (currentOldestIndex !== -1 && currentOldestId) {
                 // Find elements *after* the current oldest one in the full list
                 const start = currentOldestIndex + 1;
                 const end = Math.min(start + LOAD_MORE_COUNT, allElements.length);
                 olderElementsToLoad = allElements.slice(start, end);
             } else if (currentOldestId) {
                  console.warn(`DMM: Could not find index for current oldest message ${currentOldestId}... Might be deleted or list changed.`);
                  linkElement.textContent = 'Erreur index'; linkElement.style.color = '#aaa'; linkElement.onclick = (e) => e.preventDefault();
                  cData.isLoadingOlder = false; // Unlock
                  return;
              } else {
                  console.warn(`DMM: Cannot load older - current oldest ID is null/invalid.`);
                  cData.allMessagesLoaded = true; // Assume loaded if we have no starting point
                  if(linkElement.parentElement) linkElement.parentElement.remove();
                  cData.isLoadingOlder = false; // Unlock
                  return;
              }


             if (olderElementsToLoad.length > 0) {
                 let fetchedData = []; let fetchPromises = [];
                 for (const el of olderElementsToLoad) {
                     const parsed = parseMessageElement(el);
                     // Ensure we don't re-add a bubble that might already exist somehow (defensive check)
                     if (parsed && !container.querySelector(`.chat-bubble[data-message-id="${parsed.id}"]`)) {
                         let msgData = { ...parsed, content: null };
                         fetchedData.push(msgData);
                         fetchPromises.push(new Promise((resolve) => {
                             fetchMessageContent(msgData.id, conversationId, (content) => {
                                 const targetMsg = fetchedData.find(m => m.id === msgData.id);
                                 if (targetMsg) targetMsg.content = content; resolve();
                             });
                         }));
                     }
                 }

                 if (fetchPromises.length > 0) await Promise.all(fetchPromises);

                 fetchedData.reverse(); // Reverse so oldest fetched appears first (top)

                 const oldScrollHeight = container.scrollHeight; const oldScrollTop = container.scrollTop;

                 let newOldestId = cData.oldestMessageId; // Start with current oldest
                 fetchedData.forEach(msg => {
                     // Pass 'false' for the isInitialLoad parameter when loading older
                     const prependedId = addBubble(msg, container, conversationId, true, false); // prepend=true, isInitialLoad=false
                     // Update the overall oldest ID if the prepended one is older
                     if (prependedId && (!newOldestId || (parseInt(prependedId) < parseInt(newOldestId)))) {
                         newOldestId = prependedId;
                     }
                 });

                 // Restore scroll position relative to the old top content
                 if(fetchedData.length > 0) {
                      const newScrollHeight = container.scrollHeight;
                      container.scrollTop = oldScrollTop + (newScrollHeight - oldScrollHeight);
                 }

                 cData.oldestMessageId = newOldestId; // Update the tracked oldest message ID

                 // Check if we've reached the end of the conversation history
                 const endReached = currentOldestIndex !== -1 ? (currentOldestIndex + 1 + olderElementsToLoad.length) >= allElements.length : false;
                 if (endReached || olderElementsToLoad.length < LOAD_MORE_COUNT) { // Also assume end reached if we fetched less than requested
                     cData.allMessagesLoaded = true;
                     if (linkElement.parentElement) linkElement.parentElement.remove(); // Remove the link
                 }
             } else if (!cData.allMessagesLoaded && currentOldestId) {
                 // If no elements were found to load, but we thought there were more
                 console.log(`DMM LoadOlder: No older elements found after index ${currentOldestIndex}. Marking as fully loaded.`);
                 cData.allMessagesLoaded = true;
                 if (linkElement.parentElement) linkElement.parentElement.remove();
             }

        } catch (error) {
             console.error(`DMM: Error loading older messages for ${conversationId}:`, error);
             linkElement.textContent = 'Erreur chargement'; linkElement.style.color = 'red';
             linkElement.style.cursor = 'default'; linkElement.onclick = (e) => e.preventDefault(); // Prevent retries on error
        } finally {
             cData.isLoadingOlder = false;
             // Restore link text if still loading and no error occurred and not fully loaded
             if (!cData.allMessagesLoaded && linkElement.classList.contains('loading') && !linkElement.textContent.includes('Erreur')) {
                 linkElement.classList.remove('loading');
                 linkElement.textContent = 'Afficher les messages précédents';
             } else if (cData.allMessagesLoaded && linkElement.parentElement) {
                 // Ensure link is removed if finally block confirms all loaded
                 linkElement.parentElement.remove();
             }
        }
    }


    let closeChatWindow = (conversationId, options = { removeOriginal: true }) => {
        const customWindowId = `custom-chat-${conversationId}`;
        const chatWindow = document.getElementById(customWindowId);
        if (chatWindow) { chatWindow.remove(); }
        const cData = ACTIVE_CONVERSATIONS[conversationId];
        if (cData) {
            const oRef = cData.originalWindow;
             // Only remove original if requested AND it wasn't specifically revealed for 'invite' action
             const shouldRemove = options.removeOriginal && oRef?.dataset.modernized !== 'revealed_for_invite';
            if (shouldRemove && oRef?.parentNode) {
                try { oRef.remove(); } catch (e) { console.warn(`DMM: Error removing original window ${conversationId} on close:`, e); }
            }
            delete ACTIVE_CONVERSATIONS[conversationId];
             // console.log(`DMM: Closed window and deleted data for conversation ${conversationId}. Original removed: ${shouldRemove}`);
        }
    };


    function populateConversationSettingsUI(conversationId, checkbox, colorInput) {
        const setting = getConversationSetting(conversationId);
        checkbox.checked = setting.enabled;
        colorInput.value = setting.enabled ? setting.color : getSavedGlobalThemeColor(); // Show global if disabled
        colorInput.disabled = !setting.enabled;
    }


     function createCustomWindow(conversationId, otherParticipantName_UNUSED, initialResult, originalWindowRef) {
         const { messages, participants, totalMessages, latestId: initialLatestId, oldestId: initialOldestId, allLoaded } = initialResult;
         const windowId = `custom-chat-${conversationId}`;
         if (document.getElementById(windowId)) {
             // If DMM window already exists, just update the original ref and hide the new one.
             const existingData = ACTIVE_CONVERSATIONS[conversationId];
             if (existingData) {
                 existingData.originalWindow = originalWindowRef;
             }
             if (originalWindowRef?.parentNode) {
                 originalWindowRef.classList.add('hidden-original-databox'); // Hide the duplicate original
                 originalWindowRef.dataset.modernized = 'replaced'; // Mark as handled
             }
             console.warn(`DMM: createCustomWindow called for existing ID ${conversationId}. Updated original ref.`);
             // Focus existing window?
             const existingWindow = document.getElementById(windowId);
             if(existingWindow) {
                 existingWindow.style.zIndex = (parseInt(window.getComputedStyle(existingWindow).zIndex) || 999999) + 1; // Bring to front
                 // Focus textarea?
                 const txtArea = existingWindow.querySelector('.custom-chat-reply textarea');
                 if (txtArea) txtArea.focus();
             }
            return; // Don't create a new window
         }


         // --- Create Window Structure ---
         const chatWindow = document.createElement('div'); chatWindow.id = windowId; chatWindow.classList.add('custom-chat-window'); applyCurrentTheme(chatWindow, conversationId); // Initial theme application
         const head = document.createElement('div'); head.classList.add('custom-chat-head'); let actualTitleText = `Conversation ${conversationId}`; const originalTitleElement = originalWindowRef?.querySelector('.head .title'); if (originalTitleElement) { actualTitleText = originalTitleElement.textContent.trim(); } else { actualTitleText = `Messages ${conversationId}`; } const title = document.createElement('span'); title.classList.add('title'); title.textContent = actualTitleText; title.title = actualTitleText; const controls = document.createElement('div'); controls.classList.add('controls'); const themeBtn = document.createElement('span'); themeBtn.classList.add('theme-btn'); themeBtn.innerHTML = '🎨'; themeBtn.title = 'Changer la couleur GLOBALE du thème'; controls.appendChild(themeBtn); const participantsBtn = document.createElement('span'); participantsBtn.classList.add('participants-btn'); participantsBtn.innerHTML = '👥'; participantsBtn.title = 'Afficher les participants'; controls.appendChild(participantsBtn); const moreOptsBtn = document.createElement('span'); moreOptsBtn.classList.add('more-opts-btn'); moreOptsBtn.innerHTML = '⋮'; moreOptsBtn.title = "Plus d'options"; controls.appendChild(moreOptsBtn); const closeBtn = document.createElement('span'); closeBtn.innerHTML = '×'; closeBtn.title = 'Fermer'; controls.appendChild(closeBtn); head.appendChild(title); head.appendChild(controls); chatWindow.appendChild(head);

         const content = document.createElement('div'); content.classList.add('custom-chat-content', 'loading'); chatWindow.appendChild(content);

         const replyDiv = document.createElement('div'); replyDiv.classList.add('custom-chat-reply'); const textarea = document.createElement('textarea'); textarea.placeholder = 'Écrire un message...'; textarea.setAttribute('aria-label', 'Message reply input'); const sendButton = document.createElement('button'); sendButton.textContent = 'Envoyer'; replyDiv.appendChild(textarea); replyDiv.appendChild(sendButton); chatWindow.appendChild(replyDiv);

         const moreOptionsMenu = document.createElement('div'); moreOptionsMenu.classList.add('more-opts-menu'); const inviteItem = document.createElement('div'); inviteItem.classList.add('menu-item'); inviteItem.textContent = 'Inviter'; inviteItem.dataset.action = 'invite'; moreOptionsMenu.appendChild(inviteItem); const markUnreadItem = document.createElement('div'); markUnreadItem.classList.add('menu-item'); markUnreadItem.textContent = 'Marquer non lu'; markUnreadItem.dataset.action = 'mark_unread'; moreOptionsMenu.appendChild(markUnreadItem); const deleteItem = document.createElement('div'); deleteItem.classList.add('menu-item'); deleteItem.textContent = 'Supprimer'; deleteItem.dataset.action = 'delete'; moreOptionsMenu.appendChild(deleteItem); const settingsHr = document.createElement('hr'); moreOptionsMenu.appendChild(settingsHr); const convoSettingsWrapper = document.createElement('div'); convoSettingsWrapper.classList.add('convo-settings-item'); const specificColorLabel = document.createElement('label'); specificColorLabel.textContent = 'Couleur Spécifique:'; specificColorLabel.htmlFor = `dmm-specific-cb-${conversationId}`; const specificColorCheckbox = document.createElement('input'); specificColorCheckbox.type = 'checkbox'; specificColorCheckbox.id = `dmm-specific-cb-${conversationId}`; specificColorCheckbox.title = 'Activer une couleur unique pour cette conversation'; const specificColorInput = document.createElement('input'); specificColorInput.type = 'color'; specificColorInput.id = `dmm-specific-color-${conversationId}`; specificColorInput.title = 'Choisir la couleur spécifique pour cette conversation'; convoSettingsWrapper.appendChild(specificColorLabel); convoSettingsWrapper.appendChild(specificColorCheckbox); convoSettingsWrapper.appendChild(specificColorInput); moreOptionsMenu.appendChild(convoSettingsWrapper); chatWindow.appendChild(moreOptionsMenu);

         const participantsPanel = document.createElement('div'); participantsPanel.classList.add('participants-panel'); const panelHeader = document.createElement('div'); panelHeader.classList.add('participants-panel-header'); panelHeader.textContent = 'Participants'; const closePanelBtn = document.createElement('span'); closePanelBtn.classList.add('close-panel-btn'); closePanelBtn.innerHTML = '×'; closePanelBtn.title = 'Fermer'; panelHeader.appendChild(closePanelBtn); participantsPanel.appendChild(panelHeader); const participantsListDiv = document.createElement('div'); participantsListDiv.classList.add('participants-panel-list'); participantsPanel.appendChild(participantsListDiv); chatWindow.appendChild(participantsPanel);

         const colorPickerPanel = document.createElement('div'); colorPickerPanel.classList.add('color-picker-panel'); const colorLabel = document.createElement('label'); colorLabel.textContent = 'Couleur Globale :'; colorPickerPanel.appendChild(colorLabel); const colorInput = document.createElement('input'); colorInput.type = 'color'; colorInput.value = getSavedGlobalThemeColor(); colorPickerPanel.appendChild(colorInput); chatWindow.appendChild(colorPickerPanel);

         const resizeHandle = document.createElement('div'); resizeHandle.classList.add('resize-handle'); resizeHandle.title = 'Redimensionner'; chatWindow.appendChild(resizeHandle);


         // --- Event Listeners & State ---
         let clickOutsideMenuHandler = null; let clickOutsidePanelHandler = null; let clickOutsideColorPickerHandler = null;

         // Function and Listeners to Clear Notification
         const clearNotification = () => {
             const convData = ACTIVE_CONVERSATIONS[conversationId];
             // Check flag *before* removing class to avoid unnecessary DOM manipulation and ensure data exists
             if (convData && convData.hasUnreadNotification) {
                  convData.hasUnreadNotification = false;
                  chatWindow.classList.remove('has-unread-notification');
                  // console.log(`%cDMM: Cleared unread notification for ${conversationId} via interaction.`, "color: green"); // Optional debug log
             }
         };
         // Add listeners to the main chat window to detect interaction
         chatWindow.addEventListener('mousedown', clearNotification, true); // Use capture phase for mousedown to catch clicks early
         chatWindow.addEventListener('focusin', clearNotification); // Catches focus entering the window or its children (like textarea)


         const closeThisChatWindow = (options = { removeOriginal: true }) => {
             // Remove interaction listeners when closing
             chatWindow.removeEventListener('mousedown', clearNotification, true);
             chatWindow.removeEventListener('focusin', clearNotification);

             // Original close logic
             closeChatWindow(conversationId, options);
             if (clickOutsideMenuHandler) { document.removeEventListener('click', clickOutsideMenuHandler, true); clickOutsideMenuHandler = null; }
             if (clickOutsidePanelHandler) { document.removeEventListener('click', clickOutsidePanelHandler, true); clickOutsidePanelHandler = null; }
             if (clickOutsideColorPickerHandler) { document.removeEventListener('click', clickOutsideColorPickerHandler, true); clickOutsideColorPickerHandler = null; }
         };

         const closeOtherPopups = (except) => { if (except !== 'menu' && moreOptionsMenu.style.display === 'block') { moreOptionsMenu.style.display = 'none'; if (clickOutsideMenuHandler) document.removeEventListener('click', clickOutsideMenuHandler, true); clickOutsideMenuHandler = null; } if (except !== 'panel' && participantsPanel.classList.contains('active')) { participantsPanel.classList.remove('active'); if (clickOutsidePanelHandler) document.removeEventListener('click', clickOutsidePanelHandler, true); clickOutsidePanelHandler = null; } if (except !== 'color' && colorPickerPanel.style.display === 'block') { colorPickerPanel.style.display = 'none'; if (clickOutsideColorPickerHandler) document.removeEventListener('click', clickOutsideColorPickerHandler, true); clickOutsideColorPickerHandler = null; } };

         closeBtn.onclick = () => closeThisChatWindow({ removeOriginal: true });

         sendButton.onclick = () => {
             const messageText = textarea.value.trim();
             const cData = ACTIVE_CONVERSATIONS[conversationId];
             if (!messageText || sendButton.disabled) { if (!messageText) textarea.focus(); return; }
             if (!cData) { console.error(`DMM SendError[${conversationId}]: Conversation data not found!`); alert("Erreur critique DMM: Données de conversation manquantes."); return; }

             const currentOriginalWindow = cData.originalWindow;
             // Robust check for original window before proceeding
             if (!currentOriginalWindow || !document.body.contains(currentOriginalWindow) || !currentOriginalWindow.querySelector('.zone_reponse textarea[name=nm_texte]') || !currentOriginalWindow.querySelector('.zone_reponse .btnTxt[onclick*="sendMessage"]')) {
                 console.error(`DMM SendError[${conversationId}]: Original window reference lost, detached, or incomplete. Refresh might be needed.`);
                 alert("Erreur DMM: Référence à la fenêtre originale perdue ou invalide. La page pourrait nécessiter un rafraîchissement ou la réouverture de la conversation.");
                 sendButton.disabled = true;
                 sendButton.textContent = 'Erreur Orig.';
                 return;
             }

             // console.log(`%cDMM Send[${conversationId}]: Preparing to send message by clicking original button.`, "color: blue");
             sendButton.disabled = true;
             const originalButtonText = sendButton.textContent;
             sendButton.textContent = 'Envoi...';
             let sendAttemptError = null;
             let originalClickSuccess = false; // Flag to track if original click happened

             const originalTextarea = currentOriginalWindow.querySelector('.zone_reponse textarea[name=nm_texte]');
             const originalSendButton = currentOriginalWindow.querySelector('.zone_reponse .btnTxt[onclick*="sendMessage"]');

             // Check again just before use (belt-and-suspenders)
             if (originalTextarea && originalSendButton) {
                 try {
                     originalTextarea.value = messageText;
                     originalSendButton.click(); // Attempt to click original button
                     originalClickSuccess = true; // Mark success
                     // console.log(`%cDMM Send[${conversationId}]: Clicked original send button. Clearing input and initiating immediate refresh.`, "color: blue");
                     textarea.value = ''; // Clear DMM input *immediately* after successful click
                 } catch (e) {
                     sendAttemptError = e; // Store error
                     console.error(`%cDMM SendError[${conversationId}]: Error clicking original send button.`, "color: red", e);
                     sendButton.textContent = 'Erreur Envoi';
                 }
             } else {
                 sendAttemptError = new Error("Could not find original send elements just before use.");
                 console.error(`%cDMM SendError[${conversationId}]: ${sendAttemptError.message}`, "color: red");
                 sendButton.textContent = 'Erreur Config';
             }

             // Trigger OpenMessage immediately AFTER the original click attempt
             if (originalClickSuccess) {
                 // console.log(`%cDMM Send[${conversationId}]: Triggering immediate GM_xmlhttpRequest for OpenMessage...`, "color: #8A2BE2");
                 GM_xmlhttpRequest({
                     method: "GET",
                     url: `https://www.dreadcast.net/Menu/Messaging/action=OpenMessage&id_conversation=${conversationId}`,
                     timeout: 10000, // Shorter timeout might be okay here
                     onload: function(response) {
                         const currentSendButton = document.querySelector(`#custom-chat-${conversationId} .custom-chat-reply button`);
                         if (response.status === 200 && response.responseText) {
                             // console.log(`%cDMM Send[${conversationId}]: Immediate OpenMessage successful. Calling handler to update UI.`, "color: green");
                             try {
                                 const latestCData = ACTIVE_CONVERSATIONS[conversationId];
                                 if (latestCData && latestCData.customWindow && document.body.contains(latestCData.customWindow)) {
                                     handleOpenMessageResponse(conversationId, response.responseText); // Use existing handler
                                 } else {
                                     // console.log(`%cDMM Send[${conversationId}]: Window closed before immediate OpenMessage response could be processed.`, "color: gray");
                                 }
                                 if (currentSendButton) {
                                     currentSendButton.textContent = originalButtonText; // Restore text on success
                                     currentSendButton.disabled = false; // Re-enable
                                 }
                             } catch (handlerError) {
                                 console.error(`%cDMM SendError[${conversationId}]: Error in handleOpenMessageResponse after immediate fetch.`, "color: red", handlerError);
                                 if (currentSendButton) {
                                     currentSendButton.textContent = 'Erreur MàJ'; // Update Error
                                     setTimeout(() => { // Re-enable after delay
                                         const btn = document.querySelector(`#custom-chat-${conversationId} .custom-chat-reply button`);
                                         if (btn) { btn.disabled = false; btn.textContent = originalButtonText; }
                                     }, 2000);
                                 }
                             }
                         } else {
                             console.warn(`%cDMM Send[${conversationId}]: Immediate OpenMessage failed. Status: ${response.status}. UI will update later via /Check.`, "color: orange");
                             sendAttemptError = new Error(`Immediate OpenMessage fetch failed with status ${response.status}`); // Store error info
                             if (currentSendButton) {
                                 currentSendButton.textContent = 'MàJ différée'; // Delayed Update text
                                 setTimeout(() => { // Re-enable after delay and restore text
                                     const btn = document.querySelector(`#custom-chat-${conversationId} .custom-chat-reply button`);
                                     if (btn) { btn.disabled = false; btn.textContent = originalButtonText; }
                                 }, 2500);
                             }
                         }
                     },
                     onerror: function(error) {
                         console.error(`%cDMM SendError[${conversationId}]: Network error during immediate OpenMessage fetch.`, "color: red", error);
                         sendAttemptError = new Error("Network error during immediate update fetch."); // Store error info
                         const currentSendButton = document.querySelector(`#custom-chat-${conversationId} .custom-chat-reply button`); // Re-find button
                         if (currentSendButton) {
                             currentSendButton.textContent = 'Erreur Réseau MàJ';
                             setTimeout(() => { // Re-enable after delay and restore text
                                 const btn = document.querySelector(`#custom-chat-${conversationId} .custom-chat-reply button`);
                                 if (btn) { btn.disabled = false; btn.textContent = originalButtonText; }
                             }, 2500);
                         }
                     },
                     ontimeout: function() {
                         console.warn(`%cDMM Send[${conversationId}]: Timeout during immediate OpenMessage fetch. UI will update later.`, "color: orange");
                         sendAttemptError = new Error("Timeout during immediate update fetch."); // Store error info
                         const currentSendButton = document.querySelector(`#custom-chat-${conversationId} .custom-chat-reply button`); // Re-find button
                         if (currentSendButton) {
                             currentSendButton.textContent = 'Timeout MàJ';
                             setTimeout(() => { // Re-enable after delay and restore text
                                 const btn = document.querySelector(`#custom-chat-${conversationId} .custom-chat-reply button`);
                                 if (btn) { btn.disabled = false; btn.textContent = originalButtonText; }
                             }, 2500);
                         }
                     }
                 });
             } else {
                 // If the original click failed, reset the button state after a delay
                 setTimeout(() => {
                     const currentSendButton = document.querySelector(`#custom-chat-${conversationId} .custom-chat-reply button`);
                     if (currentSendButton) {
                         currentSendButton.disabled = false;
                         currentSendButton.textContent = originalButtonText;
                     }
                     if (sendAttemptError && !originalClickSuccess) {
                          alert(`Erreur DMM: Échec de l'envoi initial du message.\n${sendAttemptError.message}`);
                     }
                 }, 1500);
             }
         }; // --- END of sendButton.onclick handler ---

         textarea.addEventListener('keypress', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendButton.click(); } });

         // --- Other UI Handlers (Popups, Theme, Participants, Options, Resize, Drag) ---
         clickOutsideColorPickerHandler = (event) => { if (!colorPickerPanel.contains(event.target) && !themeBtn.contains(event.target)) { closeOtherPopups('none'); } };
         themeBtn.addEventListener('click', (event) => { event.stopPropagation(); const isDisplayed = colorPickerPanel.style.display === 'block'; closeOtherPopups(isDisplayed ? 'none' : 'color'); colorInput.value = getSavedGlobalThemeColor(); colorPickerPanel.style.display = isDisplayed ? 'none' : 'block'; if (!isDisplayed) { setTimeout(() => { document.addEventListener('click', clickOutsideColorPickerHandler, true); }, 0); } else { if (clickOutsideColorPickerHandler) { document.removeEventListener('click', clickOutsideColorPickerHandler, true); clickOutsideColorPickerHandler = null; } } });
         colorInput.addEventListener('input', (event) => { saveGlobalThemeColor(event.target.value); });
         clickOutsideMenuHandler = (event) => { if (!moreOptionsMenu.contains(event.target) && !moreOptsBtn.contains(event.target)) { closeOtherPopups('none'); } };
         moreOptsBtn.addEventListener('click', (event) => { event.stopPropagation(); const isDisplayed = moreOptionsMenu.style.display === 'block'; closeOtherPopups(isDisplayed ? 'none' : 'menu'); if (!isDisplayed) { populateConversationSettingsUI(conversationId, specificColorCheckbox, specificColorInput); } moreOptionsMenu.style.display = isDisplayed ? 'none' : 'block'; if (!isDisplayed) { setTimeout(() => { document.addEventListener('click', clickOutsideMenuHandler, true); }, 0); } else { if (clickOutsideMenuHandler) { document.removeEventListener('click', clickOutsideMenuHandler, true); clickOutsideMenuHandler = null; } } });
         moreOptionsMenu.addEventListener('click', (event) => {
             const menuItem = event.target.closest('.menu-item');
             if (menuItem) {
                 const action = menuItem.dataset.action;
                 closeOtherPopups('none');
                 // console.log(`DMM: Action '${action}' triggered for conversation ${conversationId}`);
                 try {
                     const cData = ACTIVE_CONVERSATIONS[conversationId];
                     const currentOriginalWindow = cData?.originalWindow;
                     const messagerie = unsafeWindow?.nav?.getMessagerie();
                     if (!messagerie && (action === 'mark_unread' || action === 'delete')) { console.error("DMM Error: unsafeWindow.nav.getMessagerie() is not accessible!"); alert("Erreur: Fonctionnalité de messagerie non trouvée."); return; }

                     if (action === 'invite') {
                         if (currentOriginalWindow && document.body.contains(currentOriginalWindow) && typeof unsafeWindow?.$ === 'function') {
                             try {
                                 // Reveal original window for invite
                                 const originalWindowId = `#${currentOriginalWindow.id}`;
                                 currentOriginalWindow.classList.remove('hidden-original-databox');
                                 currentOriginalWindow.style.opacity = ''; currentOriginalWindow.style.pointerEvents = '';
                                 currentOriginalWindow.style.zIndex = '9999'; // Bring to front
                                 currentOriginalWindow.style.top = ''; currentOriginalWindow.style.left = ''; // Reset position
                                 currentOriginalWindow.dataset.modernized = 'revealed_for_invite'; // Mark state

                                 // Animate and focus
                                 unsafeWindow.$(`${originalWindowId} .contenu`).animate({height: '140px'}, 'fast');
                                 const $zoneReponse = unsafeWindow.$(`${originalWindowId} .zone_reponse`);
                                 $zoneReponse.slideDown('fast');
                                 unsafeWindow.$(`${originalWindowId} .autres_actions`).addClass('small');
                                 $zoneReponse.find('.cible').slideDown('fast'); // Show target input

                                 // Close the DMM window *without* removing the original
                                 closeThisChatWindow({ removeOriginal: false });

                                 // Focus after animation and DMM close
                                 setTimeout(() => {
                                     const targetTextarea = currentOriginalWindow.querySelector('.zone_reponse textarea[name="nm_texte"]');
                                     if(targetTextarea) targetTextarea.focus();
                                     // console.log(`DMM: Revealed original window ${currentOriginalWindow.id} for Invite action.`);
                                 }, 400);

                             } catch (e) {
                                 console.error(`DMM: Error executing Invite action jQuery...`, e);
                                 alert("Erreur lors de la préparation de l'invitation.");
                                 closeThisChatWindow({ removeOriginal: true }); // Close DMM window *and* remove original on error
                             }
                         } else {
                             console.error("DMM Error: Cannot perform Invite - original window ref missing or jQuery not available.");
                             alert("Erreur: Impossible d'exécuter l'action 'Inviter'.");
                             closeThisChatWindow({ removeOriginal: true });
                         }
                     } else if (action === 'mark_unread') {
                         messagerie.notReadMessage(conversationId);
                         // console.log(`DMM: Called notReadMessage(${conversationId})`);
                         setTimeout(() => closeThisChatWindow({ removeOriginal: true }), 100); // Close after action
                     } else if (action === 'delete') {
                         messagerie.deleteMessage(conversationId);
                         // console.log(`DMM: Called deleteMessage(${conversationId})`);
                         setTimeout(() => closeThisChatWindow({ removeOriginal: true }), 100); // Close after action
                     }
                 } catch (e) { console.error(`DMM Error executing action '${action}'...`, e); alert(`Une erreur est survenue lors de l'action ${action}`); }
             } else if (event.target.closest('.convo-settings-item')) {
                 // Handle clicks within the settings item (e.g., label triggering checkbox)
                 if (event.target.tagName === 'INPUT' && event.target.type === 'checkbox') { /* Handled by its own listener */ }
                 else if (event.target.tagName === 'INPUT' && event.target.type === 'color') { /* Handled by its own listener */ }
                 else if (event.target.tagName === 'LABEL') {
                     const checkbox = document.getElementById(event.target.htmlFor);
                     if (checkbox && checkbox.type === 'checkbox') checkbox.click();
                 }
             }
         });
         specificColorCheckbox.addEventListener('change', (event) => { const isEnabled = event.target.checked; const currentColor = specificColorInput.value; specificColorInput.disabled = !isEnabled; setConversationSetting(conversationId, { enabled: isEnabled, color: currentColor }); applyCurrentTheme(chatWindow, conversationId); });
         specificColorInput.addEventListener('input', (event) => { const newColor = event.target.value; if (!specificColorInput.disabled) { setConversationSetting(conversationId, { enabled: true, color: newColor }); applyCurrentTheme(chatWindow, conversationId); } });
         clickOutsidePanelHandler = (event) => { if (!participantsPanel.contains(event.target) && !participantsBtn.contains(event.target)) { closeOtherPopups('none'); } };
         participantsBtn.addEventListener('click', (event) => { event.stopPropagation(); const cData = ACTIVE_CONVERSATIONS[conversationId]; if (!cData) return; const isActive = participantsPanel.classList.contains('active'); closeOtherPopups(isActive ? 'none' : 'panel'); const listHtml = cData.participants.length > 0 ? cData.participants.join('<br>') : '<p style="color:#888;font-style:italic;">Aucun participant trouvé.</p>'; participantsListDiv.innerHTML = listHtml; participantsPanel.classList.toggle('active'); if (!isActive) { setTimeout(() => { document.addEventListener('click', clickOutsidePanelHandler, true); }, 0); } else { if (clickOutsidePanelHandler) { document.removeEventListener('click', clickOutsidePanelHandler, true); clickOutsidePanelHandler = null; } } });
         closePanelBtn.addEventListener('click', () => { closeOtherPopups('none'); });
         head.addEventListener('dblclick', (e) => { if (e.target.closest('.controls span') || e.target.closest('.participants-panel') || e.target.closest('.color-picker-panel') || e.target.closest('.more-opts-menu') || e.target.classList.contains('resize-handle')) return; const isCollapsed = chatWindow.classList.toggle('collapsed'); if (isCollapsed) { closeOtherPopups('none'); } });

         // --- Final Setup ---
         document.body.appendChild(chatWindow);
         makeDraggable(chatWindow);
         makeResizable(chatWindow, resizeHandle);

         // Build UI *after* attaching event listeners etc.
         const { latestId, oldestId } = buildInitialChatUI(messages, content, conversationId, allLoaded);

         // Store conversation state
         ACTIVE_CONVERSATIONS[conversationId] = {
             customWindow: chatWindow,
             originalWindow: originalWindowRef, // Store reference to original
             latestMessageId: latestId ?? initialLatestId,
             oldestMessageId: oldestId ?? initialOldestId,
             allMessagesLoaded: allLoaded,
             isLoadingOlder: false,
             participants: participants,
             hasUnreadNotification: false // Initialize notification state
         };

         // Focus textarea after a short delay
         setTimeout(() => textarea.focus(), 200);

     } // --- END of createCustomWindow function ---


    // --- Click Simulation Functions ---
    function simulateClick(element) {
        return new Promise(resolve => {
            if (!element || !document.body.contains(element)) {
                console.warn("DMM simulateClick (.click()): Null or detached element"); resolve(false); return;
            }
            try { element.click(); resolve(true); }
            catch (e) { console.error(`DMM simulateClick (.click()): Error calling .click() on`, element, e); resolve(false); }
        });
    }
    async function initiateDoubleClick(selector, container = document) {
        const logPrefix = `DMM initiateDoubleClick (.click() x2) (${selector}):`;
        let element = null;
        try {
            element = container.querySelector(selector);
            if (!element || !document.body.contains(element)) { console.warn(`${logPrefix} Initial element not found or not in document.`); return false; }
            element.click(); // First click
            await new Promise(r => setTimeout(r, REFIND_DELAY));
            element = null; // Reset before re-find
            element = container.querySelector(selector); // RE-FIND ELEMENT
            if (!element || !document.body.contains(element)) { console.warn(`${logPrefix} Element could not be re-found after delay.`); return false; }
            element.click(); // Second click
            // console.log(`%c${logPrefix} Double .click() sequence completed.`, "color: green");
            return true;
        } catch (error) { console.error(`${logPrefix} Error during double .click() simulation:`, error); return false; }
    }


    // --- Core Logic Handlers ---

    // Function with Retries to handle UI simulation
    async function handleNewMessageEvent(conversationId, folderId) {
        const MAX_ATTEMPTS = 5; // Max number of retries
        const RETRY_DELAY = 100; // Delay between retries in milliseconds
        const logPrefix = `DMM /Check Sim Handler [${conversationId}/${folderId}]:`;
        const conversationData = ACTIVE_CONVERSATIONS[conversationId];
        let menuWasOpenedByScriptOnSuccessfulAttempt = false; // Track if menu was opened by the attempt that *succeeded*
        let overallSuccess = false; // Flag to track if any attempt succeeded

        // --- PRE-CHECK: Only proceed if the DMM window for this convo exists and is open ---
        // This check is now primarily handled *before* calling this function, but keep as a safeguard
        if (!conversationData || !conversationData.customWindow || !document.body.contains(conversationData.customWindow)) {
             console.log(`%c${logPrefix} Safeguard Check: DMM window is NOT active or attached. Aborting simulation.`, "color: gray");
             return; // Do nothing if the user doesn't have this chat open
        }

        for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
            // console.log(`%c${logPrefix} Attempt ${attempt}/${MAX_ATTEMPTS}...`, "color: #4682B4; font-weight: bold;");
            let currentAttemptMenuOpened = false; // Reset for each attempt

            try {
                // --- Step 1: Ensure Main Message Menu is Visible ---
                const messageListContainer = document.getElementById('liste_messages');
                const mainMenuButton = document.getElementById('display_messagerie');
                const isListVisibleCheck = () => messageListContainer && document.body.contains(messageListContainer) && messageListContainer.offsetParent !== null && getComputedStyle(messageListContainer).visibility !== 'hidden' && getComputedStyle(messageListContainer).display !== 'none';

                if (!isListVisibleCheck()) {
                    if (!mainMenuButton) {
                        console.error(`${logPrefix} Attempt ${attempt}: Message list not visible and #display_messagerie not found! Aborting attempts.`);
                        return; // Critical failure, stop retrying
                    }
                    // console.log(`%c${logPrefix} Attempt ${attempt}: Main message list not visible. Clicking #display_messagerie...`, "color: #4682B4");
                    currentAttemptMenuOpened = true; // Mark that *this attempt* opened the menu
                    await new Promise(r => setTimeout(r, UI_CLICK_DELAY));
                    const click1Success = await simulateClick(mainMenuButton);
                    if (!click1Success) {
                        console.warn(`${logPrefix} Attempt ${attempt}: Failed to simulate click on #display_messagerie. Retrying...`);
                        await new Promise(r => setTimeout(r, RETRY_DELAY));
                        continue; // Go to next attempt
                    }
                    await new Promise(r => setTimeout(r, UI_WAIT_DELAY)); // Wait for potential animation
                    try {
                        await waitForElement('#liste_messages'); // Wait for list to appear
                        // console.log(`%c${logPrefix} Attempt ${attempt}: Main message list should now be visible.`, "color: #4682B4");
                    } catch (waitError) {
                        console.warn(`${logPrefix} Attempt ${attempt}: Main message list (#liste_messages) did not become visible after click. Retrying...`, waitError?.message || waitError);
                        await new Promise(r => setTimeout(r, RETRY_DELAY));
                        continue; // Go to next attempt
                    }
                } // else: Menu already visible for this attempt

                // --- Step 2: Ensure Correct Folder List is Visible (Style change fallback) ---
                const folderListULSelector = '#liste_messages ul#folder_list';
                let folderListUL = document.querySelector(folderListULSelector);
                const isFolderListVisibleCheck = () => folderListUL && folderListUL.offsetParent !== null && getComputedStyle(folderListUL).display !== 'none';
                if (!isFolderListVisibleCheck() && folderListUL) {
                     // console.log(`%c${logPrefix} Attempt ${attempt}: Folder list UL not visible. Attempting direct style manipulation...`, "color: #DAA520;");
                     folderListUL.style.display = 'block';
                     await new Promise(r => setTimeout(r, UI_WAIT_DELAY / 2));
                } else if (!folderListUL) {
                     console.error(`${logPrefix} Attempt ${attempt}: Folder list UL element ('${folderListULSelector}') not found! Retrying...`);
                     await new Promise(r => setTimeout(r, RETRY_DELAY));
                     continue; // Go to next attempt
                 }

                // --- Step 3: Click Target Folder LI ---
                folderListUL = document.querySelector(folderListULSelector); // Re-select in case it was just made visible
                const targetFolderLiSelector = `li#folder_${folderId}`;
                if (!folderListUL || !document.body.contains(folderListUL)) {
                    console.warn(`${logPrefix} Attempt ${attempt}: Cannot find folder list UL ('${folderListULSelector}') or it's detached before clicking folder LI. Retrying...`);
                    await new Promise(r => setTimeout(r, RETRY_DELAY));
                    continue; // Go to next attempt
                }

                let targetFolderLi = null;
                try {
                    // Wait for the specific folder LI within the UL
                    targetFolderLi = await waitForElement(targetFolderLiSelector, WAIT_FOR_ELEMENT_TIMEOUT, folderListUL);
                } catch (findError) {
                    console.warn(`${logPrefix} Attempt ${attempt}: Failed to find target folder LI '${targetFolderLiSelector}'. Retrying...`, findError?.message || findError);
                    await new Promise(r => setTimeout(r, RETRY_DELAY));
                    continue; // Go to next attempt
                }

                // Check if folder needs clicking
                const currentFolderSpan = document.querySelector('#current_folder');
                const currentFolderId = currentFolderSpan?.dataset?.id;
                let folderClicked = false;
                if(currentFolderId !== folderId) {
                    // console.log(`%c${logPrefix} Attempt ${attempt}: Found folder LI ${folderId}. Clicking...`, "color: #4682B4");
                    await new Promise(r => setTimeout(r, UI_CLICK_DELAY));
                    const click3Success = await simulateClick(targetFolderLi);
                    if(!click3Success) {
                         console.warn(`${logPrefix} Attempt ${attempt}: Failed to simulate click on target folder LI ${folderId}. Retrying...`);
                         await new Promise(r => setTimeout(r, RETRY_DELAY));
                         continue; // Go to next attempt
                    }
                    folderClicked = true; // Mark that we clicked it
                    await new Promise(r => setTimeout(r, UI_WAIT_DELAY * 1.5)); // Wait longer after folder click for list update
                } // else { console.log(`%c${logPrefix} Attempt ${attempt}: Target folder ${folderId} already selected.`, "color: #4682B4"); }

                // --- Step 4: Find and Double-Click the Message LI ---
                const messageListContainerAgain = document.getElementById('liste_messages'); // Re-select container
                if (!messageListContainerAgain || !document.body.contains(messageListContainerAgain)) {
                    console.warn(`${logPrefix} Attempt ${attempt}: Cannot find #liste_messages container before message LI click. Retrying...`);
                    await new Promise(r => setTimeout(r, RETRY_DELAY));
                    continue; // Go to next attempt
                }
                const targetMessageSelector = `li#message_${conversationId}`;
                // console.log(`%c${logPrefix} Attempt ${attempt}: Waiting for message LI '${targetMessageSelector}'...`, "color: #4682B4");

                let targetMessageLi = null;
                try {
                    // Wait slightly longer here as list content might be loading, especially if folder was just clicked
                    targetMessageLi = await waitForElement(targetMessageSelector, WAIT_FOR_ELEMENT_TIMEOUT * (folderClicked ? 3 : 2) , messageListContainerAgain);
                } catch (findMsgError) {
                    console.warn(`${logPrefix} Attempt ${attempt}: Failed to find target message LI '${targetMessageSelector}' after folder interaction. Retrying...`, findMsgError?.message || findMsgError);
                    await new Promise(r => setTimeout(r, RETRY_DELAY));
                    continue; // Go to next attempt
                }

                // console.log(`%c${logPrefix} Attempt ${attempt}: Found message LI ${conversationId}. Initiating double-click...`, "color: #008080; font-weight: bold;");
                const success = await initiateDoubleClick(targetMessageSelector, messageListContainerAgain);

                if (success) {
                    console.log(`%c${logPrefix} Attempt ${attempt} SUCCEEDED. Double-click initiated for ${targetMessageSelector}.`, "color: green; font-weight: bold;");
                    overallSuccess = true; // Mark overall success
                    menuWasOpenedByScriptOnSuccessfulAttempt = currentAttemptMenuOpened; // Store if this successful attempt opened the menu
                    // The actual update is handled by the XHR interceptor, so we just break the retry loop.
                    break; // <<< Exit the retry loop on success >>>
                } else {
                    console.warn(`${logPrefix} Attempt ${attempt}: Double-click simulation FAILED for ${targetMessageSelector}. Retrying...`);
                    await new Promise(r => setTimeout(r, RETRY_DELAY));
                    continue; // Go to next attempt
                }

            } catch (error) {
                console.error(`${logPrefix} Attempt ${attempt}: Error during UI simulation steps. Retrying...`, error);
                await new Promise(r => setTimeout(r, RETRY_DELAY));
                continue; // Go to next attempt on general errors
            }
        } // --- END of for loop (attempts) ---

        if (!overallSuccess) {
             console.error(`${logPrefix} All ${MAX_ATTEMPTS} attempts failed to complete the UI simulation.`);
        }

        // --- Auto-close Menu (only if the *successful* attempt opened it) ---
        // This block runs outside the loop, *after* all attempts or successful break
        if (overallSuccess && menuWasOpenedByScriptOnSuccessfulAttempt) {
            // console.log(`%c${logPrefix} FINALLY: Successful attempt opened the menu. Waiting briefly before attempting to close...`, "color: #4682B4");
            await new Promise(r => setTimeout(r, 150)); // Wait a bit for potential actions

            const finalMenuButton = document.getElementById('display_messagerie');
            const finalList = document.getElementById('liste_messages');
            const finalIsListVisibleCheck = () => finalList && document.body.contains(finalList) && finalList.offsetParent !== null && getComputedStyle(finalList).visibility !== 'hidden' && getComputedStyle(finalList).display !== 'none';

            if (finalMenuButton && finalIsListVisibleCheck()) {
                 // console.log(`%c${logPrefix} FINALLY: List is still visible, attempting close click.`, "color: #4682B4");
                 await simulateClick(finalMenuButton);
                 // console.log(`%c${logPrefix} FINALLY: Clicked #display_messagerie to close menu.`, "color: #4682B4");
            } else if (finalMenuButton && !finalIsListVisibleCheck()) {
                 // console.log(`%c${logPrefix} FINALLY: Menu seems already closed. Skipping final click.`, "color: #4682B4");
             } else if (!finalMenuButton){
                  console.warn(`${logPrefix} FINALLY: Could not find #display_messagerie to attempt closing the menu.`);
              }
        } else if (overallSuccess && !menuWasOpenedByScriptOnSuccessfulAttempt) {
             // console.log(`%c${logPrefix} FINALLY: Simulation succeeded, but the successful attempt did not open the menu (it was already open). No need to close.`, "color: #4682B4");
         } else {
             // console.log(`%c${logPrefix} FINALLY: Simulation failed after all attempts. No menu closing action taken.`, "color: #4682B4");
         }
         // console.log(`%c${logPrefix} Processing finished. Overall Success: ${overallSuccess}`, "color: #4682B4; font-weight: bold;");

    } // --- END of handleNewMessageEvent (with Retries) ---


    function handleOpenMessageResponse(conversationId, responseText) {
        const conversationData = ACTIVE_CONVERSATIONS[conversationId];
        if (!conversationData || !conversationData.customWindow || !document.body.contains(conversationData.customWindow)) {
            // console.log(`%cDMM OpenMessage Handler: DMM window for ${conversationId} no longer active. Aborting UI update.`, "color: gray");
            return;
        }
        const customContentArea = conversationData.customWindow.querySelector('.custom-chat-content');
        if (!customContentArea) { console.warn(`%cDMM OpenMessage Handler[${conversationId}]: Content area not found for UI update.`, "color: red"); return; }

        const currentLatestKnownId = conversationData.latestMessageId;

        try {
            const parser = new DOMParser();
            const doc = parser.parseFromString(responseText, 'text/html');
            const latestConvList = doc.querySelector('.zone_conversation');
            if (!latestConvList) { console.warn(`%cDMM OpenMessage Handler[${conversationId}]: Could not find .zone_conversation in intercepted response. Cannot update messages.`, "color: orange"); return; }

            const serverElements = Array.from(latestConvList.querySelectorAll('.link.conversation'));
            if (serverElements.length === 0) { /* console.log(`%cDMM OpenMessage Handler[${conversationId}]: No messages found in server response.`, "color: gray"); */ return; }

            let elementsToProcess = [];
            let highestServerId = null; let highestServerIdNum = 0;

            serverElements.forEach(el => {
                const parsed = parseMessageElement(el);
                if (parsed) {
                    const elIdNum = parseInt(parsed.id);
                    if (!highestServerId || elIdNum > highestServerIdNum) { highestServerId = parsed.id; highestServerIdNum = elIdNum; }
                    const alreadyExists = customContentArea.querySelector(`.chat-bubble[data-message-id="${parsed.id}"]`);
                    const isNewer = !currentLatestKnownId || elIdNum > parseInt(currentLatestKnownId);
                    if (!alreadyExists && isNewer) { elementsToProcess.push(el); }
                    // else if (!alreadyExists && !isNewer) { /* console.log(`%cDMM OpenMessage Handler[${conversationId}]: Found message ${parsed.id} in response, not in UI, but not newer than ${currentLatestKnownId}. Adding anyway.`, "color: orange"); elementsToProcess.push(el); */ }
                }
            });

            // console.log(`%cDMM OpenMessage Handler[${conversationId}]: Server highest ID: ${highestServerId}. Client latest ID: ${currentLatestKnownId ?? 'None'}. Found ${elementsToProcess.length} potential new message(s) to add.`, "color: blue");

            if (elementsToProcess.length > 0) {
                elementsToProcess.reverse(); // Process oldest new message first

                let newlyProcessedRealIds = [];
                let fetchPromises = elementsToProcess.map(element => {
                    return new Promise(async (resolve) => {
                        const parsed = parseMessageElement(element);
                        if (parsed) {
                            // Final check before fetch completes
                            const finalConvDataCheck = ACTIVE_CONVERSATIONS[conversationId];
                            const finalContentAreaCheck = finalConvDataCheck?.customWindow?.querySelector('.custom-chat-content');
                            if (!finalConvDataCheck || !finalContentAreaCheck || finalContentAreaCheck.querySelector(`.chat-bubble[data-message-id="${parsed.id}"]`)) { resolve(); return; }

                            fetchMessageContent(parsed.id, conversationId, (content) => {
                                // Check window status *again* after async fetch returns
                                const finalConvData = ACTIVE_CONVERSATIONS[conversationId];
                                if (!finalConvData || !document.body.contains(finalConvData.customWindow)) { resolve(); return; }
                                const finalContentArea = finalConvData.customWindow.querySelector('.custom-chat-content');
                                if (!finalContentArea || finalContentArea.querySelector(`.chat-bubble[data-message-id="${parsed.id}"]`)) { resolve(); return; }

                                const msgData = { ...parsed, content: content };
                                const processedId = addBubble(msgData, finalContentArea, conversationId, false, false); // isInitialLoad=false here
                                if (processedId) newlyProcessedRealIds.push(processedId);
                                resolve();
                            });
                        } else { resolve(); }
                    });
                }); // End map

                Promise.all(fetchPromises).then(() => {
                    const postProcessConvData = ACTIVE_CONVERSATIONS[conversationId];
                    if (!postProcessConvData || !document.body.contains(postProcessConvData.customWindow)) { return; } // Final check

                    let overallLatestId = postProcessConvData.latestMessageId; let didUpdateLatest = false;
                    try {
                        newlyProcessedRealIds.forEach(processedId => {
                            if (!processedId) return;
                            const processedIdNum = parseInt(processedId);
                            const currentLatestNum = overallLatestId ? parseInt(overallLatestId) : 0;
                            if (processedIdNum > currentLatestNum) { overallLatestId = processedId; didUpdateLatest = true; }
                        });
                        if (didUpdateLatest) {
                            postProcessConvData.latestMessageId = overallLatestId;
                            // console.log(`%cDMM OpenMessage Handler[${conversationId}]: Successfully processed ${newlyProcessedRealIds.length} message(s). Updated latest ID to ${postProcessConvData.latestMessageId}`, "color: green");
                        } // else if (newlyProcessedRealIds.length > 0) { console.log(`%cDMM OpenMessage Handler[${conversationId}]: Processed ${newlyProcessedRealIds.length} message(s), but latest ID ${overallLatestId} did not change.`, "color: green"); }
                    } catch (idUpdateError) { console.error(`%cDMM OpenMessage Handler[${conversationId}]: Error updating latestMessageId after processing!`, "color: red", idUpdateError); }
                }).catch(error => { console.error(`%cDMM OpenMessage Handler[${conversationId}]: Error in Promise.all for new message fetches:`, "color: red", error); });
            } else if (highestServerId && currentLatestKnownId && parseInt(highestServerId) > parseInt(currentLatestKnownId)) {
                 // console.log(`%cDMM OpenMessage Handler[${conversationId}]: Server latest ID ${highestServerId} > Client ${currentLatestKnownId}, but no new elements added (likely already exist). Updating client latest ID.`, "color: blue");
                 conversationData.latestMessageId = highestServerId;
             }
        } catch (e) {
            console.error(`%cDMM OpenMessage Handler[${conversationId}]: General error processing intercepted response:`, "color: red", e);
        }
    } // --- END of handleOpenMessageResponse ---


    // --- Click Handling on Sidebar ---
    function handleMessageListClick(event) {
        if (!event.isTrusted) {
            // console.log("DMM ClickIntercept: Ignoring untrusted (likely simulated) event.");
            return; // Ignore clicks not directly initiated by the user
        }

        const messageLi = event.target.closest('li.message[id^="message_"]');
        if (!messageLi) return; // Click wasn't on a message LI element

        const conversationId = messageLi.id.replace('message_', '');
        const customWindowId = `custom-chat-${conversationId}`;
        const existingData = ACTIVE_CONVERSATIONS[conversationId];
        const customWindowElement = document.getElementById(customWindowId);

        // IF DMM window is currently open and associated with this conversation ID...
        if (existingData && customWindowElement && existingData.customWindow === customWindowElement && document.body.contains(customWindowElement)) {
            // console.log(`DMM ClickIntercept: User click detected on sidebar for OPEN DMM convo ${conversationId}. DMM PREVENTING default Dreadcast action.`);
            event.preventDefault(); // Stop Dreadcast from opening its own window
            event.stopPropagation(); // Stop event from bubbling further

            // Optional: Bring the existing DMM window to the front and focus its textarea
            customWindowElement.style.zIndex = (parseInt(window.getComputedStyle(customWindowElement).zIndex) || 999999) + 1;
             if (customWindowElement.classList.contains('collapsed')) { customWindowElement.classList.remove('collapsed'); } // Uncollapse
            const textarea = customWindowElement.querySelector('.custom-chat-reply textarea');
            if (textarea) setTimeout(() => textarea.focus(), 50); // Focus after a tiny delay
            return; // Stop further processing by this handler
        }

        // If an original window was previously revealed for 'invite' and is still in the DOM...
        const revealedOriginal = document.querySelector(`#db_message_${conversationId}[data-modernized="revealed_for_invite"]`);
        if (revealedOriginal && document.body.contains(revealedOriginal)) {
            // console.log(`DMM ClickIntercept: User click on message_${conversationId} while original was revealed. Hiding original before allowing default action.`);
            revealedOriginal.classList.add('hidden-original-databox'); // Re-hide the original
            revealedOriginal.dataset.modernized = ''; // Clear the state
        } // else: No DMM window open, or original not revealed. Allow default Dreadcast action.
    }
    function setupClickListener() {
        const stableParent = document.getElementById('liste_messages');
        if (stableParent) {
            stableParent.removeEventListener('click', handleMessageListClick, true); // Remove first
            stableParent.addEventListener('click', handleMessageListClick, true); // Add listener (capture phase)
            console.log("DMM: Sidebar click listener attached (capture phase).");
        } else {
            console.error("DMM: CRITICAL - #liste_messages not found for click listener. Retrying...");
            setTimeout(setupClickListener, 2000);
        }
    }


    // --- Main Observer Callback (Detects added original windows) ---
    const mainObserverCallback = async (mutationsList) => {
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList') {
                for (const node of mutation.addedNodes) {
                    // Check if the added node is an original Dreadcast message window
                    if (node.nodeType === Node.ELEMENT_NODE && node.id?.startsWith('db_message_')) {
                        const originalWindow = node;
                        const conversationId = originalWindow.id.replace('db_message_', '');

                        // Skip if already handled/marked
                        if (['processing', 'replaced', 'error', 'revealed_for_invite'].includes(originalWindow.dataset.modernized)) {
                             continue;
                        }

                        // Check if a DMM window for this conversation ALREADY exists
                        const existingDMMData = ACTIVE_CONVERSATIONS[conversationId];
                        const existingDMMWindow = document.getElementById(`custom-chat-${conversationId}`);

                        if (existingDMMData && existingDMMWindow && document.body.contains(existingDMMWindow)) {
                            // Update reference and hide original if DMM window already exists
                            // console.log(`DMM MainObserver: Detected original window ${originalWindow.id} for existing DMM window. Updating reference and hiding original.`);
                            existingDMMData.originalWindow = originalWindow;
                            originalWindow.classList.add('hidden-original-databox');
                            originalWindow.dataset.modernized = 'replaced';
                        } else {
                            // This is a NEW conversation window to modernize
                            originalWindow.dataset.modernized = 'processing';
                            // console.log(`%cDMM MainObserver: Detected new original window ${originalWindow.id}. Modernizing...`, "color: purple");
                            originalWindow.classList.add('hidden-original-databox'); // Hide it

                            if (!MY_NAME) MY_NAME = getMyCharacterName();
                            if (!MY_NAME) {
                                console.error("DMM CRITICAL: Failed to get character name when modernizing window!");
                                alert("Erreur critique DMM: Impossible d'obtenir le nom du personnage. Impossible d'ouvrir la fenêtre de message DMM.");
                                originalWindow.classList.remove('hidden-original-databox');
                                originalWindow.style.opacity = '0.7'; originalWindow.style.border = '2px dashed red'; originalWindow.style.pointerEvents = 'auto';
                                originalWindow.dataset.modernized = 'error';
                                return; // Stop processing this node
                            }

                            // Fetch initial messages and create the DMM window
                            try {
                                const initialResult = await parseAndFetchInitialMessages(originalWindow, conversationId);
                                if (!initialResult || typeof initialResult !== 'object') { throw new Error(`parseAndFetchInitialMessages returned invalid result for ${conversationId}`); }
                                createCustomWindow(conversationId, null, initialResult, originalWindow);
                                originalWindow.dataset.modernized = 'replaced';
                                // console.log(`%cDMM MainObserver: Successfully modernized ${originalWindow.id}`, "color: purple");
                            } catch (error) {
                                console.error(`DMM MainObserver: Failed to process and modernize conversation ${conversationId}:`, error);
                                alert(`DMM Erreur: Impossible de charger la conversation ${conversationId}. La fenêtre originale reste visible (avec bordure rouge).`);
                                delete ACTIVE_CONVERSATIONS[conversationId];
                                originalWindow.classList.remove('hidden-original-databox');
                                originalWindow.style.opacity = '0.7'; originalWindow.style.border = '2px dashed red'; originalWindow.style.pointerEvents = 'auto';
                                originalWindow.dataset.modernized = 'error';
                            }
                        }
                    }
                }
            }
        }
    }; // --- END of mainObserverCallback ---


    // --- Script Initialization ---
    console.log(`DMM: Dreadcast Dynamic Messages script v${SCRIPT_VERSION} starting...`);
    addChatStyles();

    function initializeScript() {
        const essentialElements = [
             document.body,
             document.getElementById('zone_messagerie'),
             document.getElementById('txt_pseudo'),
             document.getElementById('liste_messages') // Ensure message list exists for listener
        ];

        if (essentialElements.every(el => el)) {
            console.log("DMM: Essential elements found.");
            if (!MY_NAME) MY_NAME = getMyCharacterName();
            if (!MY_NAME) { console.error("DMM: Failed to get character name on init! Aborting."); return; }

            // --- Setup XHR Wrapper ---
            if (!unsafeWindow._original_XMLHttpRequest_open && !unsafeWindow._original_XMLHttpRequest_send) {
                console.log("DMM: Applying XMLHttpRequest wrappers for /Check and OpenMessage interception...");
                const origOpen = unsafeWindow.XMLHttpRequest.prototype.open;
                const origSend = unsafeWindow.XMLHttpRequest.prototype.send;
                let requestCounter = 0;

                unsafeWindow.XMLHttpRequest.prototype.open = function(method, url) {
                    this._requestMethod = method; this._requestURL = url; this._requestId = requestCounter++;
                    return origOpen.apply(this, arguments);
                };

                unsafeWindow.XMLHttpRequest.prototype.send = function() {
                    const xhr = this;
                    const reqId = xhr._requestId; const targetUrl = xhr._requestURL;

                    const dmmReadyStateHandler = function() {
                        const rsUrl = xhr._requestURL;
                        if (xhr.readyState === 4) {
                            // console.log(`DMM XHR Done [${reqId}]: State 4 for ${rsUrl}, Status: ${xhr.status}`);
                            const currentStatus = xhr.status; let currentResponseText = null;
                            try { currentResponseText = xhr.responseText; } catch(e) { /* Ignore */ }

                            // --- Handle /Check ---
                            if (rsUrl && typeof rsUrl === 'string' && rsUrl.includes('/Check')) {
                                if (currentStatus === 200 && currentResponseText) {
                                    let match = null;
                                    try {
                                        // Regex updated slightly to be less strict about whitespace around attributes, potentially more robust
                                        match = currentResponseText.match(/<evenement\s+type="nouveau_message">.*?<folder_(\d+)\s+quantite="\d+"\s+id_conversation="(\d+)"\s*\/?>.*?<\/evenement>/s);
                                    } catch (regexError) {
                                        console.error('DMM: Error during /Check regex matching!', regexError);
                                    }

                                    if (match && match[1] && match[2]) {
                                        const folderId = match[1];
                                        const conversationId = match[2];
                                        console.log(`%cDMM XHR /Check Match [${reqId}]: Found nouveau_message event! Convo ID: ${conversationId}, Folder ID: ${folderId}`, "color: orange");

                                        // --- CHECK IF DMM WINDOW IS OPEN ---
                                        const conversationData = ACTIVE_CONVERSATIONS[conversationId];
                                        const isWindowOpen = conversationData && conversationData.customWindow && document.body.contains(conversationData.customWindow);

                                        if (isWindowOpen) {
                                            // --- DMM Window IS OPEN ---
                                            // Trigger the standard UI update mechanism (which might play its own sound via addBubble)
                                            // console.log(`%cDMM /Check: Window for ${conversationId} is OPEN. Triggering UI update via handleNewMessageEvent.`, "color: blue");
                                            try {
                                                // Use setTimeout to avoid blocking the XHR handler
                                                setTimeout(() => handleNewMessageEvent(conversationId, folderId), 0);
                                            } catch (e) {
                                                console.error(`DMM ERROR: Exception queueing handleNewMessageEvent for OPEN window ${conversationId} in folder ${folderId}`, e);
                                            }
                                        } else {
                                            // --- DMM Window IS CLOSED (or doesn't exist) ---
                                            // Play the specific sound for *unopened* messages
                                            // console.log(`%cDMM /Check: Window for ${conversationId} is CLOSED or doesn't exist. Playing UNOPENED notification sound.`, "color: #FF8C00"); // Orange color
                                            try {
                                                const audio = new Audio(UNOPENED_NOTIFICATION_SOUND_URL); // Use the NEW sound URL
                                                audio.play().catch(e => {
                                                    // Log playback errors, often due to browser interaction policies
                                                    console.warn(`DMM: Unopened notification sound playback failed (interaction might be required for sound):`, e.name, e.message);
                                                });
                                            } catch (e) {
                                                console.error("DMM: Error creating or playing unopened notification sound:", e);
                                            }
                                            // IMPORTANT: Do NOT call handleNewMessageEvent here, as there's no DMM window to update.
                                            // The default Dreadcast behavior will still update the sidebar count, etc.
                                        }
                                    }
                                } // else { /* console.warn(`DMM XHR /Check Resp [${reqId}]: Non-200 status (${currentStatus}) or no responseText.`); */ }
                            }
                            // --- Handle OpenMessage ---
                            else if (rsUrl && typeof rsUrl === 'string' && rsUrl.includes('action=OpenMessage&id_conversation=')) {
                                if (currentStatus === 200 && currentResponseText) {
                                    const conversationIdMatch = rsUrl.match(/id_conversation=(\d+)/);
                                    if (conversationIdMatch && conversationIdMatch[1]) {
                                        const conversationId = conversationIdMatch[1];
                                        // console.log(`%cDMM XHR OpenMessage Resp [${reqId}]: Intercepted successful response for ${conversationId}. Processing...`, "color: #8A2BE2");
                                        try { setTimeout(() => handleOpenMessageResponse(conversationId, currentResponseText), 0); } // Use setTimeout
                                        catch (e) { console.error(`DMM ERROR: Exception queueing handleOpenMessageResponse for ${conversationId}`, e); }
                                    }
                                } else { console.warn(`DMM XHR OpenMessage Resp [${reqId}]: Intercepted non-200 status: ${currentStatus} for ${rsUrl}`); }
                            }

                             try { xhr.removeEventListener('readystatechange', dmmReadyStateHandler); } catch(removeError) { /* ignore */ }
                        }
                    }; // --- END of dmmReadyStateHandler ---

                    try { xhr.addEventListener('readystatechange', dmmReadyStateHandler); }
                    catch(addListenerError) { console.error(`DMM: FAILED to add readystatechange listener for ${targetUrl}!`, addListenerError); }

                    // Store original methods *once*
                    if(!unsafeWindow._original_XMLHttpRequest_open) unsafeWindow._original_XMLHttpRequest_open = origOpen;
                    if(!unsafeWindow._original_XMLHttpRequest_send) unsafeWindow._original_XMLHttpRequest_send = origSend;

                    return origSend.apply(this, arguments);
                }; // --- END of send override ---
                console.log("DMM: XHR wrappers applied.");
            } else {
                console.log("DMM: XHR wrappers appear to be already applied. Skipping re-application.");
            }
            // --- End XHR Wrapper Setup ---

            // Initialize Main Observer
            if (!mainObserver) {
                mainObserver = new MutationObserver(mainObserverCallback);
                mainObserver.observe(document.body, { childList: true, subtree: true });
                console.log("DMM: Main observer initialized.");
            }

            // Setup Click Listener for the sidebar
            setupClickListener();

            console.log("DMM: Initialization complete.");
        } else {
            console.log("DMM: Waiting for essential elements... Retrying init.");
            const missing = essentialElements.map((el, i) => el ? '' : ['body', '#zone_messagerie', '#txt_pseudo', '#liste_messages'][i]).filter(Boolean);
            console.log("DMM: Missing elements:", missing.join(', '));
            setTimeout(initializeScript, 500); // Retry
        }
    } // --- END of initializeScript ---


    // --- Start Initialization ---
    if (document.readyState === 'loading') {
        window.addEventListener('DOMContentLoaded', initializeScript);
    } else {
        initializeScript(); // DOM is already ready
    }


    // --- Cleanup on page unload ---
     window.addEventListener('beforeunload', () => {
         console.log("DMM: Unloading script and cleaning up...");
         if (mainObserver) mainObserver.disconnect(); mainObserver = null;

         Object.keys(ACTIVE_CONVERSATIONS).forEach(convId => {
             try { closeChatWindow(convId, { removeOriginal: true }); } // Use close function
             catch(e) { console.warn("DMM: Error during unload window cleanup for convId:", convId, e); }
         });
         for (let key in ACTIVE_CONVERSATIONS) { delete ACTIVE_CONVERSATIONS[key]; } // Clear explicitly

         const listenerTarget = document.getElementById('liste_messages');
         if (listenerTarget) { try { listenerTarget.removeEventListener('click', handleMessageListClick, true); } catch(e){} }

         try { // Restore original XHR methods if they were stored
              const win = unsafeWindow;
              if (win._original_XMLHttpRequest_open) { win.XMLHttpRequest.prototype.open = win._original_XMLHttpRequest_open; delete win._original_XMLHttpRequest_open; }
              if (win._original_XMLHttpRequest_send) { win.XMLHttpRequest.prototype.send = win._original_XMLHttpRequest_send; delete win._original_XMLHttpRequest_send; }
              // console.log("DMM: Restored original XMLHttpRequest methods.");
         } catch (e) { console.error("DMM: Error restoring original XMLHttpRequest methods:", e); }

         console.log("DMM: Cleanup complete.");
     });

})(); // End of userscript IIFE