JVChat DEBUG

JVChat avec debug intégré

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name           JVChat DEBUG
// @description    JVChat avec debug intégré
// @author         m7r-227
// @namespace      JVChat
// @license        MIT
// @version        0.4.9-debug
// @match          https://*.jeuxvideo.com/forums/42-*
// @match          https://*.jeuxvideo.com/forums/1-*
// @grant          none
// ==/UserScript==

const _DBG = true;
const _P = '%c[JVChat]';
const _S = 'color:#00e5ff;font-weight:bold';
const _W = 'color:#ffab00;font-weight:bold';
const _E = 'color:#ff1744;font-weight:bold';
function dbg(...a) { if (_DBG) console.log(_P, _S, ...a); }
function wrn(...a) { if (_DBG) console.warn(_P, _W, ...a); }
function err(...a) { if (_DBG) console.error(_P, _E, ...a); }


function addStyle() {
    const style = document.createElement('style');
    style.textContent = `
        @font-face {
            font-family: "jvchat-icons";
            src: url(data:font/woff;base64,d09GRgABAAAAAAXkAAsAAAAAB4wAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAAFuAAAACkAAAAquPq49E9TLzIAAAS0AAAARQAAAGBAg1QHY21hcAAABPwAAABYAAAAhJQqfw1nbHlmAAABCAAAAwQAAARovY2OvmhlYWQAAARIAAAANgAAADYpthI3aGhlYQAABJQAAAAfAAAAJAzUCM1obXR4AAAEgAAAABEAAAAiEM0AAGxvY2EAAAQsAAAAGgAAABoHyAZhbWF4cAAABAwAAAAfAAAAIAEiAV5uYW1lAAAFVAAAAFIAAAB8BIgdIHBvc3QAAAWoAAAAEAAAACAADQAAeNp1UkOALEkQjcisrur5KMxkVWtYao6y9bG2bdu2cV9797rutXGa47+s7dMaffrHrfmRYzYCGREvCBxg+nj+EnsJBJRhHAB9Qzd0kfEynmy32q1GMS7G3Fe0oXSpLEL59GHRyCCD6849e8v27VvOPvePeeG60088fIw+hx/54bxwYfL7lbu9hMezl5Z5zgj/wzLXGYH1Jr9dsVsHjwekCr/iVVaFjQBYNCipkWln2kVe3a3bnf9hd4kyEwPaxxxgE0Dk9KAzHBY9VTnr4i/JyDQkD94VHXPGQ886SRd/xp+TP15868MHn7/oegBGsR2ayAlggQvQ5+suF119RLaaTiMO3FTUTEUvhZMoQ9YJpQyTlxJqjFo7XgYJBFIGjGjn66+P/+qrGbQL+APsKdgPjgaIaJ7tPXAcx9DCWLEq+rqFulGMFfOGUHdFxjBxCAdxT/QU246ytSe22plPsbiY8sewsQfKQRQm8knGT9QZP0YzjQv8wMaN6WfSG9EK/AvSpna0xvUTtTUdomHlcAznenJL5YBK5YDjFPm2r9q7n2YZ9xopVs5Zl6U3bEhfZubLLEVPlrZfxvFW263cvH1sDobI7CRBQ46QhdLMZZmoTmsQ1W3tgeq4xrAY9xkZNygaQbNdbNYz7TqDi8487b5iqVS877QzP10Un3juuVMWfhyWm+fF5LklXrM1HM9PYI+BDoMAKdpiIDxsNuiei0R9fQB1qmsADXekObM4icFotbZb1Ip2q1VHj2edl2QYSoSv9746LpoDhSgqDJjF+Kp9vj6+WgVAoA+v4GsgACLaaeyrferCk3uqRlsNntt87dX2hH31tZvtkZrYsUPURt41r7jUcS69wrQng96pqd5gcgZp+k6O7G5wSKFB9aBCaSPNiVBxL9dFkfzjlre7eKDrJh+I3dhlYqtIdiY7xdaSwN1Iwe1iDol9PYfkCaOHiO4TSoPQJJui6H9QEBKhuIS2HtLy7qitSd2nDotxoyWHaIHSEzzn/riiOTzE+2x5c7sAaMv133jaY2BkYGDgYQxi4GEAASYg5gJCBob/DGAAABK+AYIAAAAAAABRAGsAiQCwAS4BcAGhAcYB6wIQAjQAAAABAAAAAQAAHiteY18PPPUACwQAAAAAAOBi554AAAAA4GLnnv/H/vwKCgMCAAAACAACAAAAAAAAeNpjAAIWGD4LotEBABJzAN4AAAB42mNgZGBgZvjPwMDAxfD/+P/jXFxAEVTACgBxLwS0AHjaY2Bh4WacwMDKwMAWwXSGgYGhH0IzvmYwYuRgYGBiYGVmwAoC0lxTGA684nvVx8zwn4EhhjmGkQUozIiiiAkANzILFgAAAHjaY2BgYAJiZiAWAZKMYJqFoQBISzAIAEU4XvG9En6l8cr5Vf6r8lfVr1pedbzqedX3/z8DAy4Z0c+i70Vvix4UnSbaL9oj2iLaKFonWgs0GwcAALUUKS542kzIgQYCMQAA0Le2RkOA9FUhBEAyGenY3P8fDAOeh+whCukiKEyfXJXpuHxafHZzn84Kmo/N3/BUfe1+3rqXqhvaMbCsAoMhgx6DAQBDxwnBAAB42mNgZoAALgasAAABYwAOeNpjYGRgYOBiUGPQYGBycfMJYeDLSSzJY5BgYGEAgv//GeAAAG2XBV0AAAA=);
        }

        .refresh-loader {
            --size: 30px;
            --thickness: 3px;
            --value: 0;

            width: var(--size);
            aspect-ratio: 1;
            border-radius: 50%;

            background: conic-gradient(var(--jv-text-secondary) calc(var(--value) * 1%), var(--jv-block-bg-color) 0);
            position: relative;
            display: flex;
            align-items: center;
            justify-content: center;
            font: 600 calc(var(--size) * 0.25)/1.1 system-ui;
            color: var(--jv-text-secondary);
            transition: background .4s ease;
            align-self: end;
        }

        .refresh-loader::before {
            content: '';
            position: absolute;
            inset: calc(var(--thickness));
            border-radius: 50%;
            background: var(--jv-block-bg-color);
        }

        .refresh-loader__text {
            position: relative;
            pointer-events: none;
        }

        .btn-open-jvchat {
            margin-right: 0.3125rem;
        }

        #jvchat-user-notif.has-notif::after,
        #jvchat-user-mp.has-notif::after {
            z-index: 2;
            content: " " attr(data-val) "";
            color: #fff;
            line-height: 1.25rem;
            font-size: 0.9rem;
            padding: 0 .25rem;
            position: absolute;
            top: .6875rem;
            right: -.6875rem;
            background: #ff3c00;
            width: 1.1rem;
            height: 1.1rem;
            border-radius: 1rem;
        }

        #jvchat-mp-and-notif .nav-link,
        .nav-link-search,
        .account-pseudo {
            color: white !important;
        }

        .jvchat-container {
            display: flex;
            height: 100vh;
            background-color: var(--jv-bg-color);
            font-family: system-ui, sans-serif;
        }

        .jvchat-sidebar {
            flex: 0 0 15%;
            max-width: 254px;
            background-color: var(--jv-block-bg-color);
            border-right: 1px solid #2b2e30;
            padding: 1rem;
            box-sizing: border-box;
            overflow-y: auto;
            display: flex;
            flex-direction: column;
            position: relative;
        }

        .jvchat-profile {
            width: 100%;
            display: flex;
            flex-direction: column;
            align-items: center;
            position: relative;
        }

        .jvchat-username {
            margin: 0 0 0.5rem 0;
            font-size: 1.25rem;
            font-weight: 700;
            text-align: center;
            color: #fff;
        }

        .jvchat-profile-avatar {
            width: 150px;
            height: 150px;
            border-radius: 50%;
            object-fit: cover;
        }

        .jvchat-profile-actions {
            display: flex;
            gap: 0.75rem;
        }

        .jvchat-topic-heading {
            font-size: 0.875rem;
            line-height: 1.42857;
            margin: 0;
            align-self: flex-start;
        }

        .jvchat-chat {
            flex: 1;
            display: flex;
            flex-direction: column;
            height: 100%;
        }

        .jvchat-messages {
            flex: 1;
            padding: 1rem;
            overflow-y: auto;
            display: flex;
            flex-direction: column;
        }

        .jvchat-message {
            display: flex;
            gap: 0.75rem;
            background: var(--jv-block-bg-color);
            padding: 0.75rem;
            border-radius: 0.5rem;
            border: 0.0625rem solid var(--jv-border-color);
        }

        .jvchat-message:nth-of-type(2n) {
            background: var(--jv-block-even-bg-color);
        }

        .jvchat-content p:last-of-type {
            margin-bottom: 0px;
        }

        .jvchat-message-controls {
            display: flex;
            align-items: center;
            gap: 0.75rem;
            visibility: hidden;
        }

        .jvchat-message-controls span {
            cursor: pointer;
        }

        .jvchat-message:hover .jvchat-message-controls {
            visibility: visible;
        }

        .jvchat-avatar {
            object-fit: cover;
            width: 40px;
            height: 40px;
            border-radius: 50%;
            flex-shrink: 0;
        }

        .jvchat-message-body {
            flex: 1;
            color: var(--jv-text-color)
        }

        .jvchat-message-header {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            justify-content: space-between;
            gap: 0.5rem;
            margin-bottom: 0.25rem;
        }

        .jvchat-user {
            font-weight: 600;
        }

        .jvchat-date {
            font-size: 0.75rem;
            opacity: 0.7;
        }

        .jvchat-form {
            padding: 0 1rem;
            /*padding: 1rem;*/
            padding-top: 0;
        }

        .messageEditor__edit {
            height: 7rem ! important;
            line-height: normal;
        }

        .jvchat-badge {
            position: absolute;
            top: 8px;
            right: -10px;
            min-width: 16px;
            padding: 1px 4px;
            font: 16px/16px Arial, sans-serif;
            color: #fff;
            background: #ff3c00;
            border-radius: 9999px;
            text-align: center;
            z-index: 10;
            pointer-events: none;
        }

        .jvchat-settings-panel {
            position: absolute;
            inset: 0;
            background-color: var(--jv-block-bg-color);
            transform: translateX(-100%);
            transition: transform 0.35s cubic-bezier(.4,0,.2,1);
            z-index: 2000;
            display: flex;
            flex-direction: column;
            gap: .75rem;
            padding: 1rem 1.25rem;
            box-shadow: 2px 0 12px rgba(0,0,0,.15);
            overflow-y: auto;
        }

        .jvchat-settings-panel.open {
            transform: translateX(0);
        }

        #jvchat-settings-list {
            display: flex;
            flex-direction: column;
            gap: .75rem;
        }

        .jvchat-message-deleted > div {
            opacity: 0.2;
            filter: grayscale(100%);
        }

        .jvchat-message-deleted {
            position: relative;
        }

        .jvchat-message-deleted:after {
            content: "Message supprimé";
            position: absolute;
            top: 40%;
            left: 50%;
            color: gray;
            font-weight: bold;
            opacity: 0.7;
        }
        
        .messageEditor__containerPreview,
        .buttonsEditor__groupPreview {
           display:none;
        }

        .jvchat-message-deleted .jvchat-message-controls {
            display: none;
        }
    `;
    document.head.appendChild(style);
}

const linkF = [...document.querySelectorAll('.spreadContainer nav.breadcrumb > a[href^="/forums/0-"]')].pop();
const hrefForum = linkF.href;
const NameForum = linkF.textContent.trim();

(function createJVChatButton() {
    const html = '<button class="buttonsNavbar__button btn-open-jvchat" type="button"><i class="buttonsNavbar__icon icon-topics-list"></i><div class="buttonsNavbar__label">JVChat</div></button>';

    function init() {
        const refreshBtn = document.querySelector('.buttonsNavbar .buttonsNavbar__icon.icon-refresh').parentNode;
        refreshBtn.insertAdjacentHTML('beforebegin', html);
        const button = document.querySelector('.btn-open-jvchat');
        button.addEventListener('click', () => {
            const jvchat = new JVChat();
            jvchat.start();
        });
    }

    // If buttonsNavbar already exists, init immediately
    if (document.querySelector('.buttonsNavbar .buttonsNavbar__icon.icon-refresh')) return init();
    // Wait for React to render the buttonsNavbar
    const observer = new MutationObserver((mutations, obs) => {
        if (document.querySelector('.buttonsNavbar .buttonsNavbar__icon.icon-refresh')) {
            obs.disconnect();
            init();
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });
    // Fallback timeout: if buttonsNavbar never appears abort
    setTimeout(() => observer.disconnect(), 5000);
})();

class JVChat {
    constructor() {
        this.jvcApi = null;
        this.jvcClient = null;

        this.lastPage = 1;
        this.messages = [];
        this.refreshRate = 6000;
        this.isTabActive = true;
        this.unreadMessagesCount = 0;
        this.autoScrollingEnabled = true;
    }

    async start() {
        addStyle();
        this.jvcClient = new JVCClient();
        const info = this.jvcClient.parseURL(location.href);
        this.topicId = info.topicId;
        this.forumId = info.forumId;
        this.viewId = info.viewId;
        this.topicTitle = info.title;
        this.jvcApi = new JVCAPI(this.viewId, this.forumId, this.topicId, this.topicTitle);
        this.jvchatSettings = new JVChatSettings();

        this.createJVChatInterface();

        const page = parsePage(document);
        this.lastPage = page.lastPage;
        dbg(`start() | lastPage initial = ${this.lastPage}`);

        // Charger les 2 dernières pages au démarrage (40 msgs)
        await this.loadInitialMessages();

        const loader = document.querySelector('.refresh-loader');
        let lastRefresh = performance.now();

        setInterval(() => {
            const now = performance.now();
            const elapsed = now - lastRefresh;
            const percent = (elapsed / this.refreshRate) * 100;
            //loader.style.setProperty('--value', percent);

            if (elapsed >= this.refreshRate) {
                lastRefresh = now;
                this.fetchNewMessages();
            }
        }, 50);

        document.addEventListener('visibilitychange', () => {
            this.isTabActive = document.visibilityState === 'visible';
            if (this.isTabActive) {
                this.unreadMessagesCount = 0;
                this.jvcClient.updateFaviconWithCount(0);
                this.scrollToBottom();
                this.autoScrollingEnabled = true;
            }
        });
    }

    async loadInitialMessages() {
        dbg(`📦 Chargement initial — lastPage=${this.lastPage}`);
        const t0 = performance.now();

        try {
            // Si le topic a au moins 2 pages, charger l'avant-dernière d'abord
            if (this.lastPage >= 2) {
                const prevPageNum = this.lastPage - 1;
                dbg(`📦 Chargement page ${prevPageNum} (avant-dernière)`);
                const prevDoc = await this.jvcApi.getPageDocument(prevPageNum);
                const prevPage = parsePage(prevDoc);

                for (const message of prevPage.messages) {
                    this.addMessage(message);
                }
                dbg(`📦 Page ${prevPageNum}: ${prevPage.messages.length} messages chargés`);
            }

            // Charger la dernière page
            dbg(`📦 Chargement page ${this.lastPage} (dernière)`);
            const lastDoc = await this.jvcApi.getPageDocument(this.lastPage);
            const lastPage = parsePage(lastDoc);

            this.payload = await getPayload(lastDoc);
            this.jvcApi.payload = this.payload;
            this.lastPage = lastPage.lastPage;

            this.connectedUser = this.getConnectedUser(lastDoc);

            document.querySelector('#jvchat-topic-title').textContent = lastPage.title;
            const nbConnectePayload = this.payload.forumInfo.header.btnVal;
            document.querySelector('#jvchat-connected-count').textContent = nbConnectePayload + (nbConnectePayload === 1 ? ' connecté' : ' connectés');
            //document.querySelector('#jvchat-connected-count').textContent = lastPage.connectedCount + (lastPage.connectedCount === 1 ? ' connecté' : ' connectés');
            document.querySelector('#jvchat-messages-count').textContent = lastPage.messagesCount + (lastPage.messagesCount === 1 ? ' message' : '  messages');

            const profileContainer = document.querySelector('.jvchat-profile');
            if (this.connectedUser) {
                profileContainer.style.display = '';
                document.querySelector('#jvchat-username').textContent = this.connectedUser.username;
                document.querySelector('#jvchat-user-avatar').src = this.connectedUser.avatarUrl;

                const messageBadge = document.querySelector('#jvchat-user-mp-badge');
                if (this.connectedUser.messageCount > 0) {
                    messageBadge.textContent = this.connectedUser.messageCount;
                    messageBadge.style.display = '';
                } else {
                    messageBadge.style.display = 'none';
                }
                const notificationBadge = document.querySelector('#jvchat-user-notif-badge');
                if (this.connectedUser.notificationCount > 0) {
                    notificationBadge.textContent = this.connectedUser.notificationCount;
                    notificationBadge.style.display = '';
                } else {
                    notificationBadge.style.display = 'none';
                }

                const profileAnchors = document.querySelectorAll('.jvchat-profile-url');
                for (const anchor of profileAnchors) {
                    anchor.href = this.connectedUser.profileUrl;
                }
                document.querySelector('.jvchat-subscriptions-url').href = this.connectedUser.subscriptionsUrl;
            } else {
                profileContainer.style.display = 'none';
            }

            for (const message of lastPage.messages) {
                this.addMessage(message);
            }

            this.scrollToBottom();

            const dt = (performance.now() - t0).toFixed(0);
            dbg(`📦 Chargement initial OK (${dt}ms) | ${this.messages.length} messages au total | lastPage=${this.lastPage}`);
        } catch (error) {
            err(`📦 Chargement initial ❌:`, error.message);
            // Fallback: lancer le polling normal qui chargera au moins la dernière page
        }
    }

    async fetchNewMessages() {
        const cycle = (this._cycle = (this._cycle || 0) + 1);
        const prevLastPage = this.lastPage;
        const t0 = performance.now();

        dbg(`━━━ Cycle #${cycle} ━━━ polling page ${this.lastPage}`);

        try {
            const doc = await this.jvcApi.getPageDocument(this.lastPage);
            const page = parsePage(doc);

            this.payload = await getPayload(doc);
            this.jvcApi.payload = this.payload;

            // ═══ Mise à jour de lastPage (provisoire, avant le check page pleine) ═══
            this.lastPage = page.lastPage;

            // ═══ DEBUG: payload check ═══
            if (!this.payload) {
                err(`Cycle #${cycle} payload est NULL !`);
            } else if (!this.payload.ajaxToken) {
                wrn(`Cycle #${cycle} payload sans ajaxToken`);
            }

            document.querySelector('#jvchat-topic-title').textContent = page.title;
            const nbConnectePayload = this.payload.forumInfo.header.btnVal;
            document.querySelector('#jvchat-connected-count').textContent = `${nbConnectePayload} connecté${nbConnectePayload === 1 ? '' : 's'}`;
            //document.querySelector('#jvchat-connected-count').textContent = page.connectedCount + (page.connectedCount === 1 ? ' connecté' : ' connectés');
            document.querySelector('#jvchat-messages-count').textContent = page.messagesCount + (page.messagesCount === 1 ? ' message' : '  messages');

            this.connectedUser = this.getConnectedUser(doc);

            const profileContainer = document.querySelector('.jvchat-profile');
            if (this.connectedUser) {
                profileContainer.style.display = '';
                document.querySelector('#jvchat-username').textContent = this.connectedUser.username;
                document.querySelector('#jvchat-user-avatar').src = this.connectedUser.avatarUrl;

                const messageBadge = document.querySelector('#jvchat-user-mp-badge');
                if (this.connectedUser.messageCount > 0) {
                    messageBadge.textContent = this.connectedUser.messageCount;
                    messageBadge.style.display = '';
                } else {
                    messageBadge.style.display = 'none';
                }
                const notificationBadge = document.querySelector('#jvchat-user-notif-badge');
                if (this.connectedUser.notificationCount > 0) {
                    notificationBadge.textContent = this.connectedUser.notificationCount;
                    notificationBadge.style.display = '';
                } else {
                    notificationBadge.style.display = 'none';
                }

                const profileAnchors = document.querySelectorAll('.jvchat-profile-url');
                for (const anchor of profileAnchors) {
                    anchor.href = this.connectedUser.profileUrl;
                }
                document.querySelector('.jvchat-subscriptions-url').href = this.connectedUser.subscriptionsUrl;
            } else {
                profileContainer.style.display = 'none';
            }

            let newMsgCount = 0;
            for (const message of page.messages) {
                const existed = this.messages.some(m => m.id === message.id);
                this.addMessage(message);
                if (!existed) newMsgCount++;
            }

            if (newMsgCount > 0) {
                dbg(`➕ ${newMsgCount} nouveaux messages ajoutés`);
            }

            // ═══ FIX PRINCIPAL v3 ═══
            // La pagination JVC est injectée côté client (JS), absente du HTML brut.
            // Quand la page courante est pleine (20 msgs), on probe la page suivante.
            // Si JVC retourne la page N+1 → elle existe, on avance.
            // Si JVC redirige vers la page N → elle n'existe pas encore, on reste.
            if (page.messages.length >= 20) {
                try {
                    const nextPageNum = this.lastPage + 1;
                    const nextDoc = await this.jvcApi.getPageDocument(nextPageNum);
                    const nextPageData = parsePage(nextDoc);

                    // Si le numéro de page retourné est supérieur, la page existe
                    if (nextPageData.lastPage > this.lastPage) {
                        this.lastPage = nextPageData.lastPage;
                        dbg(`📄 Page suivante ${this.lastPage} existe → avance`);

                        // Traiter aussi les messages de la nouvelle page
                        for (const msg of nextPageData.messages) {
                            this.addMessage(msg);
                        }
                    }
                } catch (e) {
                    // Fetch de la page suivante échoué, on reste sur la page courante
                    dbg(`📄 Probe page suivante échoué: ${e.message}`);
                }
            }

            if (this.lastPage !== prevLastPage) {
                dbg(`📄 lastPage: ${prevLastPage} → ${this.lastPage}`);
            }

            const lastMessages = this.messages.slice(this.messages.length - 20, this.messages.length);
            for (const message of lastMessages) {
                if (message.page < this.lastPage) {
                    continue;
                }

                const existing = page.messages.find((m) => m.id === message.id);
                if (!existing && !message.isDeleted) {
                    const msg = await this.jvcApi.getMessage(message.id);
                    if (!msg) {
                        message.element.classList.add('jvchat-message-deleted');
                        message.isDeleted = true;
                        dbg(`🗑️ Message #${message.id} marqué supprimé`);
                    }
                }
            }

            if (!this.isTabActive) {
                this.jvcClient.updateFaviconWithCount(this.unreadMessagesCount);
            }

            if (this.autoScrollingEnabled) {
                this.scrollToBottom();
            }

            const dt = (performance.now() - t0).toFixed(0);
            dbg(`Cycle #${cycle} OK (${dt}ms) | lastPage=${this.lastPage} | total msgs=${this.messages.length} | autoScroll=${this.autoScrollingEnabled}`);

        } catch (error) {
            err(`Cycle #${cycle} ❌ CRASH:`, error.message);
            err(`Stack:`, error.stack);
        }
    }

    scrollToBottom() {
        requestAnimationFrame(() => {
            this.messagesContainer.scrollTo({
                top: this.messagesContainer.scrollHeight,
                behavior: 'smooth'
            });
        });
    }




    addMessage(message) {
        const existing = this.messages.find((m) => m.id === message.id);
        if (existing) {
            return;
        }

        if (!this.isTabActive) {
            this.unreadMessagesCount++;
        }



        const html = `
            <article class="jvchat-message mt-2" data-id="${message.id}">
                <div>
                    <a href="${message.profileUrl}" target="_blank">
                        <img src="${message.avatarUrl}" alt="Alice avatar" class="jvchat-avatar mb-2">
                    </a>
                </div>

                <div class="jvchat-message-body">
                    <div class="jvchat-message-header">
                        <a href="/forums/message/${message.id}" target="_blank">
                            <span class="jvchat-user">${message.username}</span>
                        </a>

                        <div class="d-flex">
                            <div class="jvchat-message-controls me-3">
                                <span class="picto-msg-quote jvchat-quote-btn" title="Citer"><span>Citer</span></span>
                                <span class="picto-msg-crayon jvchat-edit-btn" title="Editer" style="display: none;"><span>Editer</span></span>
                                <span class="picto-msg-croix jvchat-delete-btn" title="Supprimer" style="display: none;"><span>Supprimer</span></span>
                            </div>

                            <span class="jvchat-date">${message.creationDate.slice(message.creationDate.length - 8, message.creationDate.length)}</span>
                        </div>
                    </div>
                    <div class="jvchat-content text-enrichi-forum txt-msg">
                        ${message.content}
                    </div>
                </div>
            </article>
        `;

        this.messagesContainer.insertAdjacentHTML('beforeend', html);

        const messageElement = document.querySelector(`.jvchat-message[data-id="${message.id}"]`);
        message.element = messageElement;

        if (this.jvchatSettings.getSettingValue('display_page_separator')) {
            let pageSeparator = document.querySelector(`.jvchat-page-separator-${message.page}`);
            if (!pageSeparator) {
                const template = document.createElement('template');
                template.innerHTML = `
                    <div class="jvchat-page-separator-${message.page} text-center">
                        <small class="text-muted">
                            Page ${message.page}
                        </small>
                    </div>
                `.trim();
                pageSeparator = template.content.firstElementChild;
            }
            messageElement.insertAdjacentElement('afterend', pageSeparator);
        }

        //Fix JVCare
        const jvcares = [...messageElement.getElementsByClassName("JvCare")];
        for (const jvcare of jvcares) {
            const a = document.createElement("a");
            a.setAttribute("target", "_blank");
            a.setAttribute("href", this.jvcClient.jvCake(jvcare.getAttribute("class")));
            a.innerHTML = jvcare.innerHTML;
            jvcare.outerHTML = a.outerHTML;
        }

        //Fix Citation ouvrable
        const togglableQuotes = [...messageElement.querySelectorAll('.text-enrichi-forum > blockquote > blockquote')];
        for (const togglableQuote of togglableQuotes) {
            const toggleButton = document.createElement('button');
            toggleButton.classList.add('message__collapsedQuote');
            togglableQuote.insertBefore(toggleButton, togglableQuote.firstChild);
            toggleButton.addEventListener('click', () => {
                const blockQuote = toggleButton.closest('.message__blockquote');
                blockQuote.classList.toggle('message__blockquote--visible');
            });
        }

        //Fix Spoils ouvrables (collision d'IDs entre messages — toggle manuel)
        const spoilHeads = [...messageElement.querySelectorAll('.message__spoilHead')];
        for (const spoilHead of spoilHeads) {
            spoilHead.addEventListener('click', (e) => {
                e.preventDefault();
                const check = spoilHead.closest('.message__spoil').querySelector('.message__openSpoil');
                check.checked = !check.checked;
            });
        }

        if (this.connectedUser && this.connectedUser.username === message.username) {
            const deleteBtn = messageElement.querySelector('.jvchat-delete-btn');
            deleteBtn.style.display = 'block';
            deleteBtn.addEventListener('click', () => {
                this.deleteMessage(message);
            });

            const editBtn = messageElement.querySelector('.jvchat-edit-btn');
            editBtn.style.display = 'block';
            editBtn.addEventListener('click', () => {
                this.editMessage(message, messageElement);
            });
        }
        const quoteBtn = messageElement.querySelector('.jvchat-quote-btn');
        quoteBtn.addEventListener('click', async () => {
            dbg(`💬 Citation demandée pour #${message.id}`);
            try {
                // L'endpoint ajax_citation.php a été supprimé par JVC (404).
                // On extrait le texte directement du contenu HTML en mémoire.
                const txt = reverseMessage(messageElement.querySelector('.jvchat-content'), true);

                let content = `> Le ${message.creationDate} ${message.username} a écrit :\n${txt}`
                content += '\n\n';

                if (this.jvcClient.createMessageTextarea.value.trim() !== '') {
                    content = '\n\n' + content;
                }
                this.jvcClient.insertAtCursor(content);
                this.jvcClient.createMessageTextarea.focus();
                dbg(`💬 Citation insérée OK`);
            } catch (e) {
                err(`💬 Citation ❌:`, e.message, e.stack);
                this.jvcClient.alert('error', `Erreur citation : ${e.message}`);
            }
        });

        const images = messageElement.querySelectorAll('.img-shack');
        for (const image of images) {
            const parent = image.parentElement;
            const link = this.jvcClient.jvCake(parent.getAttribute('class'));
            const anchor = document.createElement('a');
            anchor.href = link;
            anchor.target = '_blank';
            anchor.appendChild(image);
            parent.insertAdjacentElement('afterend', anchor);
            parent.remove();
        }

        const mosaics = this.findMosaics(messageElement);
        for (const mosaic of mosaics) {
            const container = document.createElement('div');
            container.style.lineHeight = 0;
            mosaic[0].parentElement.insertAdjacentElement('beforebegin', container);

            for (const image of mosaic) {
                let insertLineBreak = false;
                if (image.parentElement.nextElementSibling && image.parentElement.nextElementSibling.tagName === 'BR') {
                    insertLineBreak = true;
                }
                container.appendChild(image.parentElement);
                if (insertLineBreak) {
                    container.appendChild(document.createElement('br'));
                }
            }

            const btn = document.createElement('button');
            btn.classList.add('btn', 'btn-annuler-modif-msg', 'mt-2');
            if (this.jvchatSettings.getSettingValue('hide_mosaics')) {
                btn.textContent = 'Cacher';
                container.classList.add('d-none');
            } else {
                btn.textContent = 'Afficher';
            }

            btn.addEventListener('click', () => {
                container.classList.toggle('d-none');
                if (container.classList.contains('d-none')) {
                    btn.textContent = 'Afficher';
                } else {
                    btn.textContent = 'Cacher';
                }
            });
            container.insertAdjacentElement('afterend', btn);
        }

        this.messages.push(message);
    }

    findMosaics(messageEl) {
        const tol = 6;
        const nodes = [...messageEl.querySelectorAll('img.img-shack')].map(img => {
            const { x, y, width: w, height: h } = img.getBoundingClientRect();
            return { img, x, y, w, h };
        });

        if (nodes.length < 4) return [];

        const snap = (value, buckets) => {
            const bucket = buckets.find(v => Math.abs(v - value) < tol);
            if (bucket !== undefined) return bucket;
            buckets.push(value);
            return value;
        };

        const rowVals = [];
        const colVals = [];

        nodes.forEach(n => {
            n.row = snap(n.y, rowVals);
            n.col = snap(n.x, colVals);
        });

        const adj = new Map(nodes.map(n => [n, new Set]));
        const byRow = new Map();
        const byCol = new Map();

        nodes.forEach(n => {
            (byRow.get(n.row) || byRow.set(n.row, []).get(n.row)).push(n);
            (byCol.get(n.col) || byCol.set(n.col, []).get(n.col)).push(n);
        });

        byRow.forEach(list => {
            list.sort((a, b) => a.x - b.x);
            for (let i = 0; i < list.length - 1; i++) {
                if (Math.abs(list[i + 1].x - list[i].x - list[i].w) < tol) {
                    adj.get(list[i]).add(list[i + 1]);
                    adj.get(list[i + 1]).add(list[i]);
                }
            }
        });

        byCol.forEach(list => {
            list.sort((a, b) => a.y - b.y);
            for (let i = 0; i < list.length - 1; i++) {
                if (Math.abs(list[i + 1].y - list[i].y - list[i].h) < tol) {
                    adj.get(list[i]).add(list[i + 1]);
                    adj.get(list[i + 1]).add(list[i]);
                }
            }
        });

        const mosaics = [];
        const seen = new Set();

        nodes.forEach(start => {
            if (seen.has(start)) return;

            const stack = [start];
            const component = [];

            while (stack.length) {
                const n = stack.pop();
                if (seen.has(n)) continue;
                seen.add(n);
                component.push(n);
                adj.get(n).forEach(nb => stack.push(nb));
            }

            const row = new Set(component.map(n => n.row));
            const col = new Set(component.map(n => n.col));
            if (row.size < 2 || col.size < 2) {
                return;
            }

            component.sort((a, b) => (a.row === b.row ? a.col - b.col : a.row - b.row));

            mosaics.push(component.map(n => n.img));
        });

        return mosaics;
    }

    getPageUrl(page) {
        return `https://www.jeuxvideo.com/forums/${this.viewId}-${this.forumId}-${this.topicId}-${page}-0-1-0-${this.topicTitle}.htm`;
    }

    createJVChatInterface() {
        for (const child of document.body.children) {
            child.style.display = 'none';
        }
		

        

        const html = `
            <div class="jvchat-container">
                <aside class="jvchat-sidebar">
                    <span class="breadcrumb-icon icon-config align-self-end jvchat-toggle-settings" title="Paramètres"><span>Réglages</span></span>

                    <div class="jvchat-profile" style="display: none;">
                        <a class="jvchat-profile-url" target="_blank">
                            <h2 id="jvchat-username" class="jvchat-username"></h2>
                        </a>

                        <a class="jvchat-profile-url" target="_blank">
                            <img id="jvchat-user-avatar" src="https://image.jeuxvideo.com/avatar/default.jpg" alt="Profile avatar" class="jvchat-profile-avatar">
                        </a>

                        <div class="jvchat-profile-actions">
                            <a href="https://www.jeuxvideo.com/messages-prives/boite-reception.php" target="_blank">
                                <span class="headerAccount__pm" data-val="0">
                                    <i class="icon-pm"></i>
                                    <span id="jvchat-user-mp-badge" class="jvchat-badge" style="display: none;">0</span>
                                </span>
                            </a>

                            <a class="jvchat-subscriptions-url" target="_blank">
                                <span class="headerAccount__notif" data-val="0">
                                    <i class="icon-bell-off"></i>
                                    <span id="jvchat-user-notif-badge" class="jvchat-badge" style="display: none;">0</span>
                                </span>
                            </a>

                            <button type="button" class="toggleTheme" onclick="window.jvc.toggleTheme();"></button>
                        </div>
                    </div>

                    <a href="${this.getPageUrl(1)}" class="mt-3">
                        <h3 id="jvchat-topic-title" class="jvchat-topic-heading"></h3>
                    </a>

                    <span id="jvchat-connected-count"s> </span>

                    <span id="jvchat-messages-count"s> </span>
                    <a href="${hrefForum}" class="mt-3">
                        <h3 id="jvchat-sujet-liste" class="jvchat-topic-heading">${NameForum}</h3>
                    </a>

                    <div style="flex-grow: 1;"></div>

                    <div class="refresh-loader" style="--value:0">
                        <span class="refresh-loader__text" style="display: none;">0%</span>
                    </div>

                     <div class="jvchat-settings-panel" id="jvchat-settings-panel">
                        <span class="breadcrumb-icon icon-config align-self-end jvchat-toggle-settings" title="Paramètres"><span>Réglages</span></span>

                        <div class="jvchat-settings-title">Settings</div>

                        <div id="jvchat-settings-list">

                        </div>

                        <button class="jvchat-toggle-settings btn btn-annuler-modif-msg mt-2"> Fermer </button>
                    </div>
                </aside>

                <section class="jvchat-chat">
                    <div class="jvchat-messages">

                    </div>

                    <div class="jvchat-form">

                    </div>
                </section>
            </div>
        `;

        document.body.insertAdjacentHTML('beforeend', html);

        this.messagesContainer = document.querySelector('.jvchat-messages');
        const JVCForm = document.querySelector('#forums-post-message-editor');
        const formContainer = document.querySelector('.jvchat-form');
        formContainer.appendChild(JVCForm);

        const observer = new MutationObserver(() => {
            const captcha = JVCForm.querySelector('.js-captcha-logo');
            if (captcha) {
                captcha.parentElement.parentElement.style.display = 'none';
                observer.disconnect();
            }
        });
        observer.observe(JVCForm, { attributes: true, childList: true, subtree: true });

        const postButton = JVCForm.querySelector('.postMessage');
        const editorButtons = JVCForm.querySelector('.buttonsEditor');
        editorButtons.appendChild(postButton);
        postButton.classList.add('float-end');

        const previewToggleButton = document.querySelector('.buttonsEditor__groupPreview .buttonSwitch');
        if (previewToggleButton.classList.contains('buttonSwitch--isActive') && !this.jvchatSettings.getSettingValue('display_preview_by_default')) {
            previewToggleButton.click();
        }
        if (!previewToggleButton.classList.contains('buttonSwitch--isActive') && this.jvchatSettings.getSettingValue('display_preview_by_default')) {
            previewToggleButton.click();
        }

        previewToggleButton.addEventListener('click', () => {
            setTimeout(() => {
                if (previewToggleButton.classList.contains('buttonSwitch--isActive')) {
                    JVCForm.querySelector('.messageEditor__containerPreview').classList.add('mt-3');
                }
            }, 0);
        });

        const textarea = JVCForm.querySelector('textarea#message_reponse');
        textarea.setAttribute('placeholder', 'Hop hop hop, le message ne va pas s\'écrire tout seul !');
        textarea.style.height = '';
        textarea.addEventListener('blur', () => {
            textarea.style.height = '';
        });

        const textareaObserver = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                if (mutation.attributeName === 'style') {
                    if (textarea.style.height === '160px') {
                        textarea.style.height = '';
                    }
                }
            }
        });
        textareaObserver.observe(textarea, { attributes: true, attributeFilter: ['style'] });

        postButton.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            this.createMessage(textarea.value);
        });

        JVCForm.classList.remove('mb-3');
        JVCForm.querySelector('.messageEditor__containerEdit').style.marginBottom = '0px';
        JVCForm.querySelector('.messageEditor__containerPreview').style.marginBottom = '0px';


        const settingPanel = document.querySelector('#jvchat-settings-panel');
        const settingsToggleBtns = document.querySelectorAll('.jvchat-toggle-settings');
        for (const btn of settingsToggleBtns) {
            btn.addEventListener('click', () => {
                settingPanel.classList.toggle('open');
            });
        }

        this.messagesContainer.addEventListener('scroll', () => {
            const isAtTheBottom = this.messagesContainer.scrollHeight - this.messagesContainer.scrollTop <= this.messagesContainer.clientHeight + 1;
            this.autoScrollingEnabled = isAtTheBottom;
        });

        this.setupSettings();
    }

    getConnectedUser(doc) {
        if (doc.querySelector('.headerAccount__pm')) {
            const messageCount = parseInt(doc.querySelector('.headerAccount__pm').getAttribute('data-val'));
            const notificationCount = parseInt(doc.querySelector('.headerAccount__notif').getAttribute('data-val'));
            const avatarUrl = doc.querySelector('.headerAccount__avatar').style.backgroundImage.slice(5, -2).replace('/avatar-md/', '/avatar/');
            const username = doc.querySelector('.headerAccount__pseudo').textContent.trim();
            const profileUrl = `https://www.jeuxvideo.com/profil/${username.toLowerCase()}?mode=infos`;
            const subscriptionsUrl = `https://www.jeuxvideo.com/profil/${username.toLowerCase()}?mode=abonnements`;
            return { username, avatarUrl, profileUrl, subscriptionsUrl, messageCount, notificationCount };
        }
        return null;
    }

    setupSettings() {
        const settingsList = document.querySelector('#jvchat-settings-list');

        for (const setting of this.jvchatSettings.settings) {
            const html = `
                <div class="jvchat-setting">
                    <label for="${setting.key}"> ${setting.name} </label>
                    <input type="checkbox" id="${setting.key}" ${setting.value ? 'checked' : ''} />
                </div>
            `;

            settingsList.insertAdjacentHTML('beforeend', html);

            const input = settingsList.querySelector(`#${setting.key}`);
            input.addEventListener('change', () => {
                this.jvchatSettings.setSetting(setting.key, input.checked);
            });
        }
    }

    async createMessage(message) {
        try {
            const formData = new FormData();
            formData.set('text', message);
            formData.set('topicId', this.topicId);
            formData.set('forumId', this.forumId);
            formData.set('group', 1);
            formData.set('messageId', 'undefined');
            formData.set('ajax_hash', this.payload.ajaxToken);

            for (const key in this.payload.formSession) {
                formData.append(key, this.payload.formSession[key]);
            }

            const response = await fetch('https://www.jeuxvideo.com/forums/message/add', {
                credentials: 'include',
                method: 'POST',
                mode: 'cors',
                body: formData
            });

            if (!response.ok) {
                throw new Error(await response.text());
            }

            const result = await response.json();

            if (result.errors) {
                const messages = [];
                for (const key of Object.keys(result.errors)) {
                    messages.push(result.errors[key]);
                }
                throw new Error(messages.join(' | '));
            }

            this.jvcClient.setTextAreaValue('');
            this.fetchNewMessages();
        } catch (err) {
            this.jvcClient.alert('error', err.message);
            console.error(err);
        }
    }

    async deleteMessage(message) {

        const deleteUrl = this.payload.topicActions.deleteMessageUrl;
        const deleteHash = new URLSearchParams(deleteUrl.split('?')[1]).get('ajax_hash');
        //const deleteHash = document.querySelector('#ajax_hash_moderation_forum').value;

        const url = `https://www.jeuxvideo.com/forums/message/delete?ids=${message.id}&type=delete&ajax_hash=${deleteHash}`;
        const response = await fetch(url, {
            credentials: 'include',
            method: 'POST',
            mode: 'cors'
        });

        const result = await response.json();

        if (result.errors.length > 0) {
            throw new Error(result.errors.join(' | '));
        }

        this.fetchNewMessages();
    }

    async editMessage(message, messageElement) {
        try {
            const content = await this.jvcApi.getMessageContentToUpdate(message.id);

            const messageContent = messageElement.querySelector('.jvchat-content');
            const oldMessageContent = messageContent.innerHTML;
            messageContent.innerHTML = `
                <div class="messageEditor__containerEdit">
                    <textarea class="messageEditor__edit mb-2"></textarea>
                </div>

                <div class="d-flex justify-content-start gap-2">
                    <button class="simpleButton cancelMessage">
                        <div class="cancelMessage__label">Annuler</div>
                    </button>

                    <button class="simpleButton postMessage" tabindex="1">
                        <div class="postMessage__label">Modifier</div>
                        <i class="postMessage__icon icon-post-message"></i>
                    </button>
                </div>
            `;

            const textarea = messageContent.querySelector('.messageEditor__edit');
            textarea.value = content;

            const cancelBtn = messageContent.querySelector('.cancelMessage');
            cancelBtn.addEventListener('click', async () => {
                messageContent.innerHTML = oldMessageContent;
            });

            const submitBtn = messageContent.querySelector('.postMessage');
            submitBtn.addEventListener('click', async () => {
                const html = await this.jvcApi.updateMessage(message.id, textarea.value);
                messageContent.innerHTML = html;
            });
        } catch (error) {
            err(`editMessage ❌: ${error.message}`);
        }
    }
}

function parsePage(doc) {
    const title = doc.querySelector('#title-display-container').textContent.trim();
    const connectedContainer = doc.querySelector('#forums-info-app .sideCardForum__headerExtra .sideCardForum__Link');
    let connectedCount = 0;
    if (connectedContainer) {
        connectedCount = parseInt(connectedContainer.textContent.trim());
    }

    let lastPage = 1;

    // ═══ BUG FIX: utiliser doc au lieu de document ═══
    const pageAnchors = doc.querySelectorAll('.pagination .pagination__navigation a');

    // ═══ DEBUG ═══
    const liveAnchors = document.querySelectorAll('.pagination .pagination__navigation a');
    dbg(`parsePage | doc anchors: ${pageAnchors.length} | document (live/caché) anchors: ${liveAnchors.length}`);
    if (liveAnchors.length === 0 && doc !== document) {
        dbg(`✅ Confirmation: le DOM live est caché (0 anchors). On utilise bien doc.`);
    }

    for (const anchor of pageAnchors) {
        const page = parseInt(anchor.textContent.trim());
        if (!isNaN(page) && page > lastPage) {
            lastPage = page;
        }
    }

    const currentPage = doc.querySelector('.pagination__item--current');
    if (currentPage) {
        const page = parseInt(currentPage.textContent);
        if (!isNaN(page) && page > lastPage) {
            lastPage = page;
        }
    }

    const items = doc.querySelectorAll('.bloc-liste-num-page span a');
    for (const item of items) {
        const page = parseInt(item.textContent.trim());
        if (!isNaN(page) && page > lastPage) {
            lastPage = page;
        }
    }

    const messageList = doc.querySelectorAll('#listMessages .messageUser');
    const messages = [];
    for (const message of messageList) {
        let avatarUrl = 'https://image.jeuxvideo.com/avatar/default.jpg';
        const avatar = message.querySelector('img.avatar__image');
        if (avatar) {
            avatarUrl = avatar.src;
        }
        const username = message.querySelector('.messageUser__label').textContent.trim();
        const profileUrl = `https://www.jeuxvideo.com/profil/${username.toLowerCase()}?mode=infos`;

        const id = message.id.split('-').pop();

        messages.push({
            id,
            page: lastPage,
            username,
            avatarUrl,
            profileUrl,
            content: message.querySelector('.messageUser__msg').innerHTML,
            creationDate: message.querySelector('.messageUser__date').textContent.trim()
        });
    }

    const messagesCount = (lastPage * 20) - 20 + messages.length;

    dbg(`parsePage résultat | lastPage=${lastPage} | msgs sur page=${messages.length} | total estimé=${messagesCount}`);

    return { title, connectedCount, messagesCount, lastPage, messages };
}

async function getPayload(doc) {
    try {
        const scriptPayLoad = [...doc.scripts].find(s => s.textContent?.includes('forumsAppPayload'))?.textContent;
        const rawPayload = scriptPayLoad?.match(/jvc\.forumsAppPayload\s*=\s*["']?([^"']+)["']?/)?.[1];
        if (!rawPayload) return undefined;
        try {
            const bytes = Uint8Array.from(atob(rawPayload), c => c.charCodeAt(0));
            const decompressedStream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'));
            const json = await new Response(decompressedStream).text();
            return JSON.parse(json); // GZIP + BASE 64
        } catch {
            return JSON.parse(atob(rawPayload)); // BASE 64
        }
    } catch (e) {
        console.error("Erreur extraction du payload:", e);
        return undefined;
    }
}

function reverseMessage(node, isInit, isUl) {
    let quote = "";
    let prevIsP = false;
    let startsWithSpoil = false;

    for (let child of node.childNodes) {
        let name = child.nodeName;

        switch (name) {
            case "P": {
                quote += reverseMessage(child) + "\n\n";
                break;
            }
            case "STRONG": {
                quote += "'''" + reverseMessage(child) + "'''";
                break;
            }
            case "U": {
                quote += "<u>" + reverseMessage(child) + "</u>";
                break;
            }
            case "S": {
                quote += "<s>" + reverseMessage(child) + "</s>";
                break;
            }
            case "EM": {
                quote += "''" + reverseMessage(child) + "''";
                break;
            }
            case "BR": {
                quote += "\n";
                break;
            }
            case "UL": {
                quote += reverseMessage(child, false, true) + "\n\n";
                break;
            }
            case "OL": {
                quote += reverseMessage(child, false, false) + "\n\n";
                break;
            }
            case "LI": {
                if (isUl === true) {
                    quote += "* " + reverseMessage(child) + "\n";
                } else {
                    quote += "# " + reverseMessage(child) + "\n";
                }
                break;
            }
            case "DIV": {
                let classList = child.classList;
                if (classList.contains("message__spoil")) {
                    if (quote === "") {
                        startsWithSpoil = true;
                    }
                    quote += "<spoil>" + reverseMessage(child) + "</spoil>\n\n"
                } else if (classList.contains("message__spoilContent")) {
                    quote += reverseMessage(child);
                }
                break;
            }
            case "SPAN": {
                let classList = child.classList;
                if (classList.contains("message__spoil")) {
                    quote += "<spoil>" + reverseMessage(child) + "</spoil>";
                } else if (classList.contains("message__spoilContent")) {
                    quote += reverseMessage(child);
                }
                break;
            }
            case "LABEL": {
                break;
            }
            case "INPUT": {
                break;
            }
            case "IMG": {
                quote += child.alt;
                break;
            }
            case "A": {
                // SI UN PSEUDO EST CLIQUABLE (PLUG-IN EXTERNE / DEBOUCLED) ON SKIP
                if (child.classList.contains("deboucled-highlighted")) {
                    quote += reverseMessage(child);
                } else if (child.href) {
                    quote += child.href;
                } else {
                    quote += reverseMessage(child);
                }
                break;
            }
            case "PRE": {
                quote += reverseMessage(child) + "\n\n";
                break;
            }
            case "CODE": {
                quote += "<code>" + child.textContent + "</code>";
                break;
            }
            case "BLOCKQUOTE": {
                if (prevIsP) {
                    quote = quote.trimEnd() + "\n" + reverseMessage(child).replace(/^/gm, '> ') + "\n\n";
                } else {
                    quote += reverseMessage(child).replace(/^/gm, '> ') + "\n\n";
                }

                break;
            }
            case "#text": {
                // The "isInit" check is to prevent the empty text surroudning message
                // However, it may happen that the root node contains valid text child, so it need to be added somehow
                // For some reason, an "new line" may be missing in this case, so just add it
                if (!isInit || child.textContent.trim() !== "") {
                    quote += child.textContent;
                    if (isInit && !quote.endsWith("\n")) {
                        quote += "\n";
                    }
                }
                break;
            }
            default: {
                break;
            }
        }

        if (name == "P") {
            prevIsP = true;
        } else {
            prevIsP = false;
        }
    }

    quote = quote.replace(/(\n){3,}/g, '\n\n');

    if (startsWithSpoil && isInit) {
        quote = "\n" + quote.trimEnd();
    } else {
        quote = quote.trim();
    }

    if (isInit) {
        quote = quote.replace(/^/gm, '> ');
    }

    return quote;
}

class JVCAPI {
    constructor(viewId, forumId, topicId, topicTitle) {
        this.viewId = viewId;
        this.forumId = forumId;
        this.topicId = topicId;
        this.topicTitle = topicTitle;
        this.payload = null;
    }

    async getMessageContentToUpdate(messageId) {
        try {
            const url = `https://www.jeuxvideo.com/forums/ajax_edit_message.php?id_message=${messageId}&ajax_hash=${this.payload.ajaxToken}&action=get`;
            const response = await fetch(url, {
                method: 'GET',
                credentials: 'include'
            });

            if (!response.ok) {
                throw new Error('Echec de la requête pour récupérer le message à editer');
            }

            const data = await response.json();
            if (data.errors.length === 0) {
                return data.jvcode;
            } else {
                throw new Error(data.errors[0]);
            }
        } catch (error) {
            throw new Error(error);
        }
    }

    async updateMessage(messageId, content) {
        const formData = new FormData();
        formData.set('text', content);
        formData.set('topicId', this.topicId);
        formData.set('forumId', this.forumId);
        formData.set('group', 1);
        formData.set('messageId', messageId);
        formData.set('ajax_hash', this.payload.ajaxToken);

        for (const key in this.payload.formSession) {
            formData.append(key, this.payload.formSession[key]);
        }

        try {
            const response = await fetch('https://www.jeuxvideo.com/forums/message/edit', {
                method: 'POST',
                credentials: 'include',
                body: formData
            });
            const data = await response.json();

            if (!data.errors) {
                return data.html;
            } else {
                throw new Error(data.errors[0]);
            }
        } catch (error) {
            throw new Error(error);
        }
    }

    async getMessage(messageId) {
        try {
            const url = `https://www.jeuxvideo.com/forums/message/${messageId}`;
            const response = await fetch(url);
            if (!response.ok) {
                return null;
            }
            return await response.text();
        } catch (err) {
            return null;
        }
    }

    async getPageDocument(page) {
        try {
            const url = `https://www.jeuxvideo.com/forums/${this.viewId}-${this.forumId}-${this.topicId}-${page}-0-1-0-${this.topicTitle}.htm`;
            dbg(`⬇ Fetch page ${page}: ${url}`);
            const t0 = performance.now();
            const response = await fetch(url);

            if (!response.ok) {
                throw new Error(`Erreur ${response.status}`);
            }

            const html = await response.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');
            dbg(`⬆ Page ${page} OK (${(performance.now() - t0).toFixed(0)}ms)`);
            return doc;
        } catch (error) {
            err(`⬆ Fetch page ${page} ❌:`, error.message);
            throw new Error(error);
        }
    }

    async getMessageQuote(messageId) {
        try {
            const url = `https://www.jeuxvideo.com/forums/ajax_citation.php?id_message=${messageId}&ajax_hash=${this.payload.ajaxToken}`;
            const response = await fetch(url);

            if (!response.ok) {
                throw new Error(`Erreur ${response.status}`);
            }

            const result = await response.json();
            return result.txt;
        } catch (error) {
            throw new Error(error);
        }
    }
}

class JVCClient {
    constructor() {
        const link = document.querySelector('link[rel*=\'icon\']');
        this.faviconUrl = link ? link.href : '/favicon.ico';
        this.createMessageTextarea = document.querySelector('textarea#message_reponse');
    }

    parseURL(url) {
        const matches = url.match(/^https:\/\/www\.jeuxvideo\.com\/(?:recherche\/)?forums\/(\d+)-(\d+)-(\d+)-(\d+)-(\d+)-(\d+)-(\d+)-(.*?)\.htm/);
        if (matches === null) {
            throw new Error('Url mismatch');
        }

        const viewId = parseInt(matches[1]);
        const forumId = parseInt(matches[2]);
        const topicId = parseInt(matches[3]);
        const topicPage = parseInt(matches[4]);
        const forumOffset = parseInt(matches[6]);
        const title = matches[8];

        return { viewId, forumId, topicId, topicPage, forumOffset, title };
    }

    updateFaviconWithCount(count) {
        const img = new Image();
        img.crossOrigin = 'anonymous';

        img.onload = function () {
            const canvas = document.createElement('canvas');
            const size = 16;
            canvas.width = size;
            canvas.height = size;
            const ctx = canvas.getContext('2d');

            ctx.drawImage(img, 0, 0, size, size);

            if (count > 0) {
                ctx.fillStyle = 'DodgerBlue';
                ctx.fillRect(0, 0, ctx.measureText(count).width + 3, 11);
                ctx.fillStyle = 'white';
                ctx.font = 'bold 10px Verdana';
                ctx.textBaseline = 'bottom';
                ctx.fillText(count, 1, 11);
            }

            const newFavicon = canvas.toDataURL('image/png');
            let link = document.querySelector('link[rel*=\'icon\']');
            if (!link) {
                link = document.createElement('link');
                link.rel = 'icon';
                document.head.appendChild(link);
            }
            link.href = newFavicon;
        };

        img.src = this.faviconUrl;
    }

    alert(type, message) {
        let color = 'text-white';
        let bgColor = 'bg-danger';

        switch (type) {
            case 'error':
                bgColor = 'bg-danger';
                break;
            case 'warning':
                color = 'text-black';
                bgColor = 'bg-warning';
                break;
            case 'success':
                bgColor = 'bg-success';
                break;
        }

        const id = `jvchat-alert-${Date.now()}`;

        const html = `
           <div id="${id}" class="position-fixed top-0 w-100 pe-none" style="z-index: 2147483647;">
                <div class="toast-container w-100 d-flex align-items-center flex-column p-3 pe-auto">
                    <div class="toast align-items-center ${color} ${bgColor} border-0 fade show">
                        <div class="d-flex">
                            <div class="toast-body">
                                <div>${message}</div>
                            </div>
                            <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
                        </div>
                    </div>
                </div>
            </div>
        `;

        document.body.insertAdjacentHTML('beforeend', html.trim());
        const alert = document.querySelector(`#${id}`);
        setTimeout(() => {
            alert.remove();
        }, 5000);
    }

    jvCake(str) {
        const base16 = '0A12B34C56D78E9F';
        const s = str.split(' ')[1];
        let lien = '';
        for (let i = 0; i < s.length; i += 2) {
            lien += String.fromCharCode(base16.indexOf(s.charAt(i)) * 16 + base16.indexOf(s.charAt(i + 1)));
        }
        return lien;
    }

    setTextAreaValue(value) {
        const prototype = Object.getPrototypeOf(this.createMessageTextarea);
        const nativeSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set;
        nativeSetter.call(this.createMessageTextarea, value);
        this.createMessageTextarea.dispatchEvent(new Event('input', { bubbles: true }));
    }

    insertAtCursor(textToInsert) {
        const value = this.createMessageTextarea.value;
        const start = this.createMessageTextarea.selectionStart;
        const end = this.createMessageTextarea.selectionEnd;
        const newValue = value.slice(0, start) + textToInsert + value.slice(end);
        this.setTextAreaValue(newValue);
        this.createMessageTextarea.selectionStart = this.createMessageTextarea.selectionEnd = start + textToInsert.length;
    }
}

class JVChatSettings {
    constructor() {
        this.settings = [
            {
                key: 'hide_mosaics',
                name: 'Masquer les mosaïques',
                description: 'Cache automatiquement les mosaïques d\'images NoelShack pour réduire le flooding.',
                value: true
            },
            {
                key: 'display_page_separator',
                name: 'Afficher le  numéro de page courante',
                description: '',
                value: true
            },
            {
                key: 'display_preview_by_default',
                name: 'Afficher la prévisualisation par défaut',
                description: '',
                value: false
            }
        ];

        this.loadSettings();
    }

    getSetting(key) {
        return this.settings.find((setting) => setting.key === key);
    }

    getSettingValue(key) {
        return this.getSetting(key).value;
    }

    setSetting(key, value) {
        const setting = this.getSetting(key);
        setting.value = value;
        this.saveSettings();
    }

    saveSettings() {
        for (const setting of this.settings) {
            localStorage.setItem(setting.key, setting.value);
        }
    }

    loadSettings() {
        for (const setting of this.settings) {
            const item = localStorage.getItem(setting.key);
            if (item) {
                setting.value = JSON.parse(item);
            }
        }
    }
}