Torn Chat 3.0 Integrated Search Feature

Adds search to Torn Chats.

// ==UserScript==
// @name         Torn Chat 3.0 Integrated Search Feature
// @namespace    https://greasyfork.org/en/users/1431907-theeeunknown
// @version      1.2
// @description  Adds search to Torn Chats.
// @author       TR0LL [2561502]
// @license      CC BY-SA 4.0
// @match        *://*.torn.com/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_info
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    const SCRIPT_ID = 'chat-3-0-search';
    const PANEL_ID = `${SCRIPT_ID}-panel`;
    const FOOTER_BUTTON_ID = `${SCRIPT_ID}-footer-button`;
    const FACTION_ID_STORAGE_KEY = 'chatHelper_factionId_v2';

    const config = {
        panelVisible: false,
        panelPosition: { top: '20px', right: '20px', left: 'auto' },
        targetChatIdRegex: /^(public_global$|public_trade$|public_company$|public_poker$|public_staff$|private-\d+-\d+|company-\d+|poker-|staff-)$/,
        chatWindowSelector: `#chatRoot [class*="root___"][class*="visible___"]`,
        chatListSelector: `div[class*="list___"]`,
        messageGroupSelector: `div[class*="root___r_1Ra"]`,
        messageTextSelector: `span[class*="message___pRfWR"] span[class*="root___Xw4jI"]`,
        senderSelector: `a[class*="sender___"]`,
        timestampSelector: `[class*="messageGroupTimestamp___"]`,
        chatFooterBarSelector: 'div[class*="root___oWxEV"]',
        inputAreaContainerSelector: 'div[class*="root___WUd1h"]',
        chatHeaderClass: '[class*="root___"][class*="header___"]',
        chatWindowTitleClass: '[class*="title___"]',
        RETRY_INTERVAL_MS: 3000,
        FOOTER_BUTTON_CHECK_INTERVAL_MS: 3000,
        WAIT_FOR_ELEMENT_TIMEOUT_MS: 5000,
        INITIAL_SCAN_DELAY_MS: 1500,
    };

    let enhancedPanel = null;
    let dragOffsetX, dragOffsetY;
    let activeChatWindows = {};
    let currentSearchResults = [];
    let currentSearchDetails = { query: '', chatId: '', chatName: '' };
    let forceSearchContainer = {};
    let storedFactionId = null;

    function saveSettings() {
        try {
            const settingsToSave = {
                panelPosition: (enhancedPanel && enhancedPanel.style.left !== 'auto' && enhancedPanel.style.top) ? { top: enhancedPanel.style.top, left: enhancedPanel.style.left } : config.panelPosition
            };
            GM_setValue('chatEnhancerSettings_v3_pos_only', JSON.stringify(settingsToSave));
        } catch (error) {
            console.error("EnhancedChat: Error saving settings:", error);
        }
    }
    function loadSettings() {
        try {
            const savedSettings = GM_getValue('chatEnhancerSettings_v3_pos_only');
            if (!savedSettings) return;
            const loadedSettings = JSON.parse(savedSettings);
            if (!loadedSettings || typeof loadedSettings !== 'object') return;
            if (loadedSettings.panelPosition) {
                 if (typeof loadedSettings.panelPosition.top === 'string' && loadedSettings.panelPosition.top !== 'auto' &&
                     typeof loadedSettings.panelPosition.left === 'string' && loadedSettings.panelPosition.left !== 'auto')
                 {
                     config.panelPosition = {
                         top: loadedSettings.panelPosition.top,
                         left: loadedSettings.panelPosition.left,
                         right: 'auto',
                         bottom: 'auto'
                     };
                 } else {
                     config.panelPosition = {
                         top: loadedSettings.panelPosition.top || 'auto',
                         bottom: loadedSettings.panelPosition.bottom || '60px',
                         left: loadedSettings.panelPosition.left || '20px',
                         right: loadedSettings.panelPosition.right || 'auto'
                     };
                     if(config.panelPosition.top !== 'auto') config.panelPosition.bottom = 'auto';
                     if(config.panelPosition.left !== 'auto') config.panelPosition.right = 'auto';
                 }
            }
        } catch (error) {
            console.error("EnhancedChat: Error loading settings:", error);
            config.panelPosition = { top: 'auto', bottom: '60px', left: '20px', right: 'auto' };
        }
    }
    function parseTimestampText(text) { if (!text) return null; const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1); text = text.trim(); const timeParts = (match) => ({ hours: parseInt(match[1], 10), minutes: parseInt(match[2], 10), seconds: match[3] ? parseInt(match[3], 10) : 0 }); const dateParts = (match) => ({ day: parseInt(match[1], 10), month: parseInt(match[2], 10) - 1, year: 2000 + parseInt(match[3], 10) }); if (text.startsWith('Today')) { const timeMatch = text.match(/Today (\d{1,2}):(\d{2})(?::(\d{2}))?/); if (timeMatch) { const { hours, minutes, seconds } = timeParts(timeMatch); const date = new Date(today); date.setHours(hours, minutes, seconds); return date; } return today; } if (text.startsWith('Yesterday')) { const timeMatch = text.match(/Yesterday (\d{1,2}):(\d{2})(?::(\d{2}))?/); if (timeMatch) { const { hours, minutes, seconds } = timeParts(timeMatch); const date = new Date(yesterday); date.setHours(hours, minutes, seconds); return date; } return yesterday; } const dateTimeMatch = text.match(/^(\d{2}):(\d{2}):(\d{2})\s*-\s*(\d{1,2})\/(\d{1,2})\/(\d{2})$/); if (dateTimeMatch) { try { const { hours, minutes, seconds } = timeParts(dateTimeMatch); const { day, month, year } = dateParts(dateTimeMatch.slice(4)); const date = new Date(year, month, day, hours, minutes, seconds); if (!isNaN(date) && date.getMonth() === month) return date; } catch (e) {} } const simpleDateMatch = text.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2})$/); if (simpleDateMatch) { try { const { day, month, year } = dateParts(simpleDateMatch); const date = new Date(year, month, day); if (!isNaN(date) && date.getMonth() === month) return date; } catch (e) {} } return null; }
    function escapeRegex(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }

    const searchIconSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>`;
    function applyStyles() {
        GM_addStyle(`
            #${PANEL_ID} {position:fixed; z-index:1001; width:350px; height:300px; background-color: var(--chat-color-background, #f2f2f2); border:1px solid var(--chat-color-border-primary, #ababab); border-radius:5px; box-shadow:0 3px 8px rgba(0,0,0,0.3); display:flex; flex-direction:column; color: var(--default-color, #333); font-size:12px; overflow:hidden; opacity:1; visibility:visible; transform:scale(1); transition:opacity 0.25s ease-in-out, transform 0.25s ease-in-out, visibility 0s linear 0s;}
            #${PANEL_ID}.hidden {opacity:0; transform:scale(0.98); visibility:hidden; transition:opacity 0.25s ease-in-out, transform 0.25s ease-in-out, visibility 0s linear 0.25s; pointer-events:none;}
            #${SCRIPT_ID}-panel-header {padding:5px 8px; border-bottom:1px solid var(--chat-color-border-secondary, #ccc); background:var(--title-gray-gradient, linear-gradient(180deg, #888888 0%, #444444 100%)); display:flex; justify-content:space-between; align-items:center; font-size:12px; font-weight:bold; color:var(--title-color, #FFF); border-top-left-radius:4px; border-top-right-radius:4px; text-shadow:var(--title-text-shadow, 0 1px 0 #000); cursor:grab; flex-shrink:0;}
            #${SCRIPT_ID}-panel-header:active {cursor:grabbing;}
            #${SCRIPT_ID}-panel-title {white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-right:5px;}
            #${SCRIPT_ID}-panel-close {cursor:pointer; font-weight:bold; background:none; border:none; color:#fff; font-size:16px; line-height:1; opacity:0.8; text-shadow:0 1px 0 #000; padding:0 2px;}
            #${SCRIPT_ID}-panel-close:hover {opacity:1;}
            #${SCRIPT_ID}-tabs {display:flex; flex-shrink:0; border-bottom:1px solid var(--chat-color-border-secondary, #ccc); background-color: var(--torn-tabs-active-background, #fff);}
            .enh-chat-tab {padding:6px 12px; cursor:default; border:none; font-size:11px; font-weight:bold; color: var(--torn-tabs-active-color, #333); flex-grow:1; text-align:center;}
            #${SCRIPT_ID}-content-container {flex-grow:1; display:flex; flex-direction:column; overflow:hidden;}
            .enh-chat-tab-content {display:flex; flex-direction:column; height:100%; overflow:hidden; flex-grow:1; padding:10px; background-color:#fff; color:#333;}
            #${SCRIPT_ID}-search-controls {display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; flex-shrink:0; border-bottom:1px solid var(--chat-color-border-secondary, #ccc); padding-bottom:5px;}
            #${SCRIPT_ID}-search-results-title {font-size:12px; font-weight:bold; color: var(--default-gray-6-color); margin:0; padding:0;}
            #${SCRIPT_ID}-export-button {cursor:pointer; background:var(--btn-background, linear-gradient(180deg,#DEDEDE 0%,#F7F7F7 25%,#CFCFCF 60%,#E7E7E7 78%,#D9D9D9 100%)); border:var(--btn-border, 1px solid #aaa); color:var(--btn-color, #555); border-radius:3px; padding:3px 8px; font-size:10px; text-shadow:var(--btn-text-shadow, 0 1px 0 #FFFFFF40); font-weight:normal; margin-left:5px;}
            #${SCRIPT_ID}-export-button:hover {background:var(--btn-hover-background, linear-gradient(180deg,#CCCCCC 0%,#FFFFFF 25%,#BBBBBB 60%,#EEEEEE 78%,#CCCCCC 100%));}
            #${SCRIPT_ID}-search-results {border:1px solid var(--chat-color-border-secondary, #ccc); background-color: #fff; border-radius:3px; overflow-y:auto; flex-grow:1;}
            .${SCRIPT_ID}-search-result-item {padding:5px 8px; border-bottom:1px solid #eee; font-size:11px; cursor:default; line-height:1.4; color: #333;}
            .${SCRIPT_ID}-search-result-item:hover {background-color: #f0f0f0;}
            .${SCRIPT_ID}-search-result-item:last-child {border-bottom:none;}
            .${SCRIPT_ID}-search-result-item .cloned-sender-link {font-weight:bold; margin-right:3px; color: #369;}
            .${SCRIPT_ID}-search-result-item .cloned-sender-link:hover {text-decoration:underline;}
            .${SCRIPT_ID}-search-result-highlight {background-color: #FFFF80; border-radius:2px; padding:0 1px; font-weight:bold; color: #000;}
            .${SCRIPT_ID}-search-no-results {padding:10px; text-align:center; font-style:italic; color: #666; font-size:11px;}
            .chat-search-container {padding: 3px 6px; border-top:1px solid var(--chat-color-border-secondary, #ccc); border-bottom:1px solid var(--chat-color-border-secondary, #ccc); display:flex; flex-wrap: nowrap; align-items:center; gap: 3px; background-color: var(--chat-color-background, #f0f0f0); margin-top:-1px;}
            .chat-search-container input[type=text] {flex-grow:1; height: 22px; line-height:20px; font-size:11px; border:1px solid var(--input-border-color, #ccc); color: var(--input-color, #000); background: var(--input-background-color, linear-gradient(0deg, #fff 0%, #fff 100%)); padding: 1px 5px; border-radius:3px; min-width:60px;}
            .chat-search-container input[type=text]:focus {border-color:var(--input-focus-border-color, #1864AB80); outline:none;}
            .chat-search-container button { flex-shrink:0; cursor:pointer; background:var(--btn-background, linear-gradient(180deg,#DEDEDE 0%,#F7F7F7 25%,#CFCFCF 60%,#E7E7E7 78%,#D9D9D9 100%)); border:var(--btn-border, 1px solid #aaa); color:var(--btn-color, #555); border-radius:3px; padding: 1px 3px; height: 22px; line-height:1; display:inline-flex; align-items:center; justify-content:center;}
            .chat-search-container button svg { width: 12px; height: 12px; }
            .chat-search-container button:hover {background:var(--btn-hover-background, linear-gradient(180deg,#CCCCCC 0%,#FFFFFF 25%,#BBBBBB 60%,#EEEEEE 78%,#BBBBBB 100%)); border:var(--btn-hover-border, 1px solid #999); color:var(--btn-hover-color, #444);}
            .chat-search-container .msg-count-display {font-size:9px; color: var(--default-gray-6-color, #666); margin-left:auto; padding-left:5px; flex-shrink:0; white-space:nowrap;}
            .highlighted-search-result {background-color: #FFFACD !important; border:1px solid #FFD700 !important; box-shadow:0 0 5px #FFD700 !important; border-radius:3px; margin:1px 0; transition:background-color .3s ease;}
       `);
    }

    function createPanelHeader() { const header = document.createElement('div'); header.id = `${SCRIPT_ID}-panel-header`; const title = document.createElement('span'); title.id = `${SCRIPT_ID}-panel-title`; title.textContent = 'Chat Search Results'; const closeButton = document.createElement('button'); closeButton.id = `${SCRIPT_ID}-panel-close`; closeButton.innerHTML = '&times;'; closeButton.title = 'Close Panel'; closeButton.addEventListener('click', () => togglePanelVisibility(false)); header.appendChild(title); header.appendChild(closeButton); return header; }
    function createSearchTabContent(container) { const controlsDiv = document.createElement('div'); controlsDiv.id = `${SCRIPT_ID}-search-controls`; controlsDiv.innerHTML = `<h3 id="${SCRIPT_ID}-search-results-title">Search Results</h3><button id="${SCRIPT_ID}-export-button" title="Export current results to a text file">Export Results</button>`; controlsDiv.querySelector(`#${SCRIPT_ID}-export-button`).addEventListener('click', exportSearchResults); const resultsDiv = document.createElement('div'); resultsDiv.id = `${SCRIPT_ID}-search-results`; resultsDiv.innerHTML = `<div class="${SCRIPT_ID}-search-no-results">Perform a search in a chat window.</div>`; container.appendChild(controlsDiv); container.appendChild(resultsDiv); }
    function createPanelContentContainer() { const container = document.createElement('div'); container.id = `${SCRIPT_ID}-content-container`; const tabsContainer = document.createElement('div'); tabsContainer.id = `${SCRIPT_ID}-tabs`; const searchTabBtn = document.createElement('div'); searchTabBtn.className = 'enh-chat-tab'; searchTabBtn.textContent = 'Search Results'; tabsContainer.appendChild(searchTabBtn); const tabContentArea = document.createElement('div'); tabContentArea.style.flexGrow = '1'; tabContentArea.style.overflow = 'hidden'; tabContentArea.style.display = 'flex'; const searchContent = document.createElement('div'); searchContent.id = `${SCRIPT_ID}-search-content`; searchContent.className = 'enh-chat-tab-content active'; createSearchTabContent(searchContent); tabContentArea.appendChild(searchContent); container.appendChild(tabsContainer); container.appendChild(tabContentArea); return container; }
    function createEnhancedChatPanel() { if (enhancedPanel) return enhancedPanel; enhancedPanel = document.createElement('div'); enhancedPanel.id = PANEL_ID; enhancedPanel.classList.add('hidden'); const header = createPanelHeader(); const contentContainer = createPanelContentContainer(); enhancedPanel.append(header, contentContainer); document.body.appendChild(enhancedPanel); header.addEventListener('mousedown', dragMouseDown); applyPanelPosition(); return enhancedPanel; }
    function applyPanelPosition() { if (!enhancedPanel) return; Object.assign(enhancedPanel.style, { top: '', left: '', right: '', bottom: '' }); enhancedPanel.style.top = config.panelPosition.top || 'auto'; enhancedPanel.style.left = config.panelPosition.left || 'auto'; enhancedPanel.style.right = config.panelPosition.right || 'auto'; enhancedPanel.style.bottom = config.panelPosition.bottom || 'auto'; }
    function displaySearchResultsInPanel(targetChatId, query, results) {
        console.log(`EnhancedChat: Displaying ${results.length} results for "${query}" in ${targetChatId}`);
        if (!enhancedPanel) { createEnhancedChatPanel(); if(!enhancedPanel) { console.error("EnhancedChat: Failed to create panel for displaying results."); return; } }
        const resultsDiv = document.getElementById(`${SCRIPT_ID}-search-results`);
        const resultsTitle = document.getElementById(`${SCRIPT_ID}-search-results-title`);
        if (!resultsDiv || !resultsTitle) { console.error("EnhancedChat: Panel internal elements missing for search results display."); return; }
        if (enhancedPanel.classList.contains('hidden')) { togglePanelVisibility(true); }
        const chatName = activeChatWindows[targetChatId]?.name || targetChatId.replace(/^(public_|faction-|private-\d+-|company-|poker-|staff-)/, '');
        const safeQuery = escapeRegex(query);
        const regex = new RegExp(safeQuery, 'gi');
        resultsDiv.innerHTML = '';
        resultsTitle.textContent = `Results for "${query}" in ${chatName} (${results.length})`;
        currentSearchResults = results;
        currentSearchDetails = { query: query, chatId: targetChatId, chatName: chatName };
        if (results.length > 0) { const fragment = document.createDocumentFragment(); results.forEach(result => { const resultItem = document.createElement('div'); resultItem.className = `${SCRIPT_ID}-search-result-item`; const senderContainer = document.createElement('span'); const originalSenderLink = result.senderElement; let senderHtml = ''; if (originalSenderLink && originalSenderLink.tagName === 'A') { const clonedLink = originalSenderLink.cloneNode(true); clonedLink.className = 'cloned-sender-link'; senderHtml = clonedLink.outerHTML; } else { senderHtml = `<span class="${result.senderElement?.className||''}">${result.sender}</span>`; } if (result.sender.toLowerCase().includes(query.toLowerCase())) { senderHtml = senderHtml.replace(regex, match => `<span class="${SCRIPT_ID}-search-result-highlight">${match}</span>`); } senderContainer.innerHTML = senderHtml + ':&nbsp;'; const textSpan = document.createElement('span'); textSpan.className = 'text'; const highlightedText = result.text.replace(regex, match => `<span class="${SCRIPT_ID}-search-result-highlight">${match}</span>`); textSpan.innerHTML = highlightedText; resultItem.appendChild(senderContainer); resultItem.appendChild(textSpan); fragment.appendChild(resultItem); }); resultsDiv.appendChild(fragment); } else { resultsDiv.innerHTML = `<div class="${SCRIPT_ID}-search-no-results">No results found for "${query}" in ${chatName}.</div>`; }
    }
    function exportSearchResults() { if (!currentSearchResults || currentSearchResults.length === 0) return; const { query, chatId, chatName } = currentSearchDetails; let exportText = `Torn Chat Search Results\nChat: ${chatName} (${chatId})\nQuery: "${query}"\nGenerated: ${new Date().toLocaleString()}\nResults Found: ${currentSearchResults.length}\n\n----------------------------------------\n\n`; currentSearchResults.forEach(result => { exportText += `${result.sender}: ${result.text}\n`; }); const blob = new Blob([exportText], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const safeChatName = (chatName || 'UnknownChat').replace(/[^a-z0-9_-]/gi, '_'); const safeQuery = (query || 'no_query').replace(/[^a-z0-9_-]/gi, '_').substring(0, 20); a.download = `torn_search_${safeChatName}_${safeQuery}_${Date.now()}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }
    function togglePanelVisibility(forceShow = null) { if (!enhancedPanel) { createEnhancedChatPanel(); if (!enhancedPanel) return; } let makeVisible; if (forceShow === true) makeVisible = true; else if (forceShow === false) makeVisible = false; else makeVisible = enhancedPanel.classList.contains('hidden'); enhancedPanel.classList.toggle('hidden', !makeVisible); }
    function dragMouseDown(e) { if (e.target.closest('button')) return; e.preventDefault(); dragOffsetX = e.clientX - enhancedPanel.offsetLeft; dragOffsetY = e.clientY - enhancedPanel.offsetTop; document.addEventListener('mousemove', elementDrag); document.addEventListener('mouseup', closeDragElement, { once: true }); }
    function elementDrag(e) { e.preventDefault(); let newLeft = e.clientX - dragOffsetX; let newTop = e.clientY - dragOffsetY; newTop = Math.max(0, Math.min(newTop, window.innerHeight - enhancedPanel.offsetHeight)); newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - enhancedPanel.offsetWidth)); enhancedPanel.style.top = `${newTop}px`; enhancedPanel.style.left = `${newLeft}px`; enhancedPanel.style.right = "auto"; enhancedPanel.style.bottom = "auto"; }
    function closeDragElement() { document.removeEventListener('mousemove', elementDrag); config.panelPosition = { top: enhancedPanel.style.top, left: enhancedPanel.style.left, right: 'auto', bottom: 'auto' }; saveSettings(); }

    function enhanceChatWindow(chatWindow) {
        if (!chatWindow || chatWindow.dataset.enhancedChatProcessed === 'true') return;
        const chatId = chatWindow.id;

        const shouldEnhance = (storedFactionId && chatId === storedFactionId) || config.targetChatIdRegex.test(chatId);
        if (!chatId || !shouldEnhance) return;

        const inputAreaContainer = chatWindow.querySelector(config.inputAreaContainerSelector);
        if (!inputAreaContainer || !inputAreaContainer.parentNode) {
            forceSearchContainer[chatId] = true;
            return;
        }
        const insertionPoint = inputAreaContainer.parentNode;

        if (!chatWindow.querySelector(`.${SCRIPT_ID}-search-container`)) {
            const searchContainer = document.createElement('div');
            searchContainer.className = `chat-search-container ${SCRIPT_ID}-search-container`;

            const searchInput = document.createElement('input'); searchInput.type = 'text';
            const titleElement = chatWindow.querySelector(`${config.chatHeaderClass} ${config.chatWindowTitleClass}`);
            const chatTitle = titleElement?.textContent || chatId?.replace(/^public_|^faction-|^private-.*?(-|$)|^company-|^poker-|^staff-/, '') || 'chat';
            searchInput.placeholder = `Search ${chatTitle}...`;

            const searchButton = document.createElement('button'); searchButton.innerHTML = searchIconSVG; searchButton.title = 'Search';
            const msgCountDisplay = document.createElement('span'); msgCountDisplay.className = 'msg-count-display'; msgCountDisplay.textContent = '(...)';

            searchContainer.appendChild(searchInput);
            searchContainer.appendChild(searchButton);
            searchContainer.appendChild(msgCountDisplay);

            insertionPoint.parentNode.insertBefore(searchContainer, insertionPoint);

            const handleSearch = () => {
                console.log(`EnhancedChat: handleSearch called for ${chatId}`);
                const query = searchInput.value;
                if(!query) return;
                const results = performIndividualChatSearch(chatId, query);
                displaySearchResultsInPanel(chatId, query, results);
            };
            searchButton.addEventListener('click', handleSearch);
            searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleSearch(); } });

            updateMessageCount(chatWindow);
        }

        const titleElement = chatWindow.querySelector(`${config.chatHeaderClass} ${config.chatWindowTitleClass}`);
        activeChatWindows[chatId] = { name: titleElement?.textContent || chatId.replace(/^public_|^faction-|^private-.*?(-|$)|^company-|^poker-|^staff-/, ''), element: chatWindow };
        chatWindow.dataset.enhancedChatProcessed = 'true';

        if (forceSearchContainer[chatId]) {
            delete forceSearchContainer[chatId];
        }
    }

    function performIndividualChatSearch(targetChatId, query) {
        query = query.trim().toLowerCase();
        if (!query) return [];
        const chatWindowElement = document.getElementById(targetChatId);
        if (!chatWindowElement) return [];

        const chatList = chatWindowElement.querySelector(config.chatListSelector);
        if (!chatList) return [];

        const results = [];
        const messageGroups = chatList.querySelectorAll(config.messageGroupSelector);
        console.log(`EnhancedChat: Found ${messageGroups.length} message groups in ${targetChatId}`);

        messageGroups.forEach((msgGroup) => {
            const senderElement = msgGroup.querySelector(config.senderSelector);
            const messageElement = msgGroup.querySelector(config.messageTextSelector);

            if (senderElement && messageElement) {
                const senderText = senderElement.textContent?.replace(':', '').trim() || '';
                const messageText = messageElement.textContent?.trim() || '';

                if (messageText.toLowerCase().includes(query) || senderText.toLowerCase().includes(query)) {
                    results.push({
                        element: msgGroup,
                        sender: senderText,
                        senderElement: senderElement,
                        text: messageText
                    });
                }
            }
        });
        console.log(`EnhancedChat: Found ${results.length} matches for "${query}" in ${targetChatId}`);
        return results;
    }

    function updateMessageCount(chatWindow) { const chatList = chatWindow.querySelector(config.chatListSelector); const messageCountSpan = chatWindow.querySelector(`.${SCRIPT_ID}-search-container .msg-count-display`); if (chatList && messageCountSpan) { const messageCount = chatList.querySelectorAll(config.messageGroupSelector).length; messageCountSpan.textContent = `(${messageCount} msgs)`; } }
    function cleanupChatWindow(chatWindow) { if (chatWindow?.id && activeChatWindows[chatWindow.id]) { delete activeChatWindows[chatWindow.id]; } if (chatWindow?.id && forceSearchContainer[chatWindow.id]) { delete forceSearchContainer[chatWindow.id]; } }

    async function waitForElement(selector, parent = document, timeout = config.WAIT_FOR_ELEMENT_TIMEOUT_MS) { return new Promise((resolve, reject) => { const existingElement = parent.querySelector(selector); if (existingElement) { resolve(existingElement); return; } let observer = null; let timer = null; const cleanup = () => { if (observer) observer.disconnect(); clearTimeout(timer); }; observer = new MutationObserver(() => { const element = parent.querySelector(selector); if (element) { cleanup(); resolve(element); } }); try { observer.observe(parent, { childList: true, subtree: true }); } catch (e) { cleanup(); reject(new Error(`Observer failed for ${selector} in ${parent.id || parent.tagName}: ${e.message}`)); return; } timer = setTimeout(() => { cleanup(); reject(new Error(`Element ${selector} not found within timeout ${timeout}ms in parent ${parent.id || parent.tagName}`)); }, timeout); }); }
    async function enhanceChatWindowWhenReady(chatWindow) { if (!chatWindow || !chatWindow.id || chatWindow.dataset.enhancedChatProcessed === 'true') return; const timeoutMs = config.WAIT_FOR_ELEMENT_TIMEOUT_MS; try { await Promise.all([ waitForElement(config.inputAreaContainerSelector, chatWindow, timeoutMs), waitForElement(config.chatListSelector, chatWindow, timeoutMs) ]); enhanceChatWindow(chatWindow); } catch (error) { console.warn(`${GM_info.script.name}: Initial check failed for chat ${chatWindow.id}. Flagging for retry. Error:`, error.message); forceSearchContainer[chatWindow.id] = true; } }
    function setupObservers() { const bodyObserver = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { const chatWindows = node.matches(config.chatWindowSelector) ? [node] : node.querySelectorAll(config.chatWindowSelector); chatWindows.forEach(chatWindow => { if (chatWindow.id && !chatWindow.dataset.enhancedChatProcessed && (chatWindow.id === storedFactionId || config.targetChatIdRegex.test(chatWindow.id))) { enhanceChatWindowWhenReady(chatWindow); } }); } }); mutation.removedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { const chatWindows = node.matches(config.chatWindowSelector) ? [node] : node.querySelectorAll(config.chatWindowSelector); chatWindows.forEach(cleanupChatWindow); } }); }); }); bodyObserver.observe(document.body, { childList: true, subtree: true }); const footerBar = document.querySelector(config.chatFooterBarSelector); if (footerBar) { const buttonObserver = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'attributes' && mutation.attributeName === 'class' && mutation.target.nodeName === 'BUTTON' && mutation.target.id.startsWith('channel_panel_button:')) { const button = mutation.target; const chatId = button.id.split(':')[1]; if (chatId && (chatId === storedFactionId || config.targetChatIdRegex.test(chatId))) { const chatWindow = document.getElementById(chatId); if (button.classList.contains('opened___Mwpgz') && chatWindow && !chatWindow.dataset.enhancedChatProcessed) { enhanceChatWindowWhenReady(chatWindow); } } } }); }); buttonObserver.observe(footerBar, { attributes: true, subtree: true, attributeFilter: ['class'] }); } else { console.warn("EnhancedChat: Footer bar not found, cannot observe button states."); } setupMessageObserver(); setInterval(() => { for (const chatId in forceSearchContainer) { if (forceSearchContainer[chatId] === true) { const chatWindow = document.getElementById(chatId); if (chatWindow && !chatWindow.dataset.enhancedChatProcessed && (chatId === storedFactionId || config.targetChatIdRegex.test(chatId))) { enhanceChatWindow(chatWindow); } else if (!chatWindow || chatWindow.dataset.enhancedChatProcessed) { delete forceSearchContainer[chatId]; } } } }, config.RETRY_INTERVAL_MS); setTimeout(() => { document.querySelectorAll(config.chatWindowSelector).forEach(chatWindow => { if (chatWindow.id && !chatWindow.dataset.enhancedChatProcessed && (chatWindow.id === storedFactionId || config.targetChatIdRegex.test(chatWindow.id))) { const button = document.getElementById(`channel_panel_button:${chatWindow.id}`); if (button && button.classList.contains('opened___Mwpgz')) { enhanceChatWindowWhenReady(chatWindow); } } }); }, config.INITIAL_SCAN_DELAY_MS); }
    function setupMessageObserver() { const observerCallback = (mutationsList) => { const updatedWindows = new Set(); for (const mutation of mutationsList) { if (mutation.type === 'childList') { const chatWindow = mutation.target.closest(config.chatWindowSelector); if (chatWindow && chatWindow.dataset.enhancedChatProcessed === 'true') { updatedWindows.add(chatWindow); } } } updatedWindows.forEach(win => updateMessageCount(win)); }; const listObserver = new MutationObserver(observerCallback); const bodyObserver = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { const chatLists = node.matches(config.chatListSelector) ? [node] : node.querySelectorAll(config.chatListSelector); chatLists.forEach(list => { if (!list.dataset.messageObserverAttached) { listObserver.observe(list, { childList: true }); list.dataset.messageObserverAttached = 'true'; const chatWindow = list.closest(config.chatWindowSelector); if(chatWindow) updateMessageCount(chatWindow); } }); } }); mutation.removedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { const chatLists = node.matches(config.chatListSelector) ? [node] : node.querySelectorAll(config.chatListSelector); chatLists.forEach(list => { if(list.dataset.messageObserverAttached) { delete list.dataset.messageObserverAttached; } }); } }); }); }); bodyObserver.observe(document.body, { childList: true, subtree: true }); document.querySelectorAll(config.chatListSelector).forEach(list => { if (!list.dataset.messageObserverAttached) { listObserver.observe(list, { childList: true }); list.dataset.messageObserverAttached = 'true'; const chatWindow = list.closest(config.chatWindowSelector); if(chatWindow) updateMessageCount(chatWindow); } }); }

    function createFooterButton() { if (document.getElementById(FOOTER_BUTTON_ID)) return null; const buttonContainer = document.createElement('div'); buttonContainer.className = 'root___cYD0i'; const button = document.createElement('button'); button.type = 'button'; button.className = 'root___WHFbh root___K2Yex root___RLOBS'; button.id = FOOTER_BUTTON_ID; button.title = 'Toggle Enhanced Chat Search Panel'; button.innerHTML = searchIconSVG; button.addEventListener('click', () => { togglePanelVisibility(null); }); buttonContainer.appendChild(button); return buttonContainer; }
    function ensureFooterButtonExists() { const footerBar = document.querySelector(config.chatFooterBarSelector); if (footerBar) { const existingButton = document.getElementById(FOOTER_BUTTON_ID); if (!existingButton || !footerBar.contains(existingButton)) { const buttonContainer = createFooterButton(); if (buttonContainer) { const notesButton = footerBar.querySelector('#notes_panel_button'); if (notesButton) { footerBar.insertBefore(buttonContainer, notesButton); } else { footerBar.appendChild(buttonContainer); } } } } }

    function initialize() {
        loadSettings();
        applyStyles();

        storedFactionId = GM_getValue(FACTION_ID_STORAGE_KEY, null);
        const currentPath = window.location.pathname;
        const isOnAllowedPage = currentPath.includes('/preferences.php') || currentPath.includes('/factions.php');

        if (!storedFactionId && isOnAllowedPage) {
             let enteredId = window.prompt("Enter Faction ID (Numbers only):", "");
             if (enteredId && /^\d+$/.test(enteredId.trim())) {
                 storedFactionId = 'faction-' + enteredId.trim();
                 GM_setValue(FACTION_ID_STORAGE_KEY, storedFactionId);
                 console.log(`EnhancedChat: Stored Faction ID: ${storedFactionId}`);
             } else if (enteredId !== null) {
                 window.alert("Invalid Faction ID entered (only numbers allowed). Faction chat search will not be enabled until a valid ID is provided on the preferences or faction page.");
                 storedFactionId = null;
             } else {
                 storedFactionId = null;
             }
        } else if (storedFactionId) {
             console.log(`EnhancedChat: Loaded stored Faction ID: ${storedFactionId}`);
        }

        ensureFooterButtonExists();
        setInterval(ensureFooterButtonExists, config.FOOTER_BUTTON_CHECK_INTERVAL_MS);

        setupObservers();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }

})();