JVCHAT

Version de Shiho corrigée (manque plus que la gestion du scroll auto...)
 avatar
unknown
javascript
3 days ago
70 kB
95
No Index
// ==UserScript==
// @name           JVChat DEBUG
// @description    JVChat avec debug intégré
// @author         m7r-227 - captain_cid31
// @namespace      JVChat
// @license        MIT
// @version        0.2.4-debug
// @match          https://*.jeuxvideo.com/forums/42-*
// @match          https://*.jeuxvideo.com/forums/1-*
// @grant          none
// ==/UserScript==

// downloadURL    https://update.greasyfork.org/scripts/574211/JVChat%20DEBUG.user.js
// updateURL      https://update.greasyfork.org/scripts/574211/JVChat%20DEBUG.meta.js

const _DBG = false ;
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 {
            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;

        /* MODIF PERSO : CONSERVER LES BOUTONS CITER ET MODIFIER POUR LES MESSAGES SUPPRIMÉS */
            pointer-events: none !important;
        }

        .messageEditor__containerPreview,
        .buttonsEditor__groupPreview {
           display:none;
        }



        /* MODIF PERSO : CONSERVER LES BOUTONS CITER ET MODIFIER POUR LES MESSAGES SUPPRIMÉS */

        .jvchat-message-deleted .jvchat-message-controls span:not(.jvchat-quote-btn):not(.jvchat-edit-btn) {
            display: none !important;
        }

        .jvchat-message-deleted .jvchat-quote-btn,
        .jvchat-message-deleted .jvchat-edit-btn {
            display: inline-block !important;
            opacity: 1 !important;
            pointer-events: auto !important;
            position: relative;
            z-index: 100; /* Pour passer au dessus de l'overlay "supprimé" */
        }

        .jvchat-message-deleted .jvchat-message-controls {
            visibility: visible !important;
            opacity: 1 !important;
        }
    `;
    document.head.appendChild(style);
}

(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>';

    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();
    });
})();

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 (max 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 = getPayload(lastDoc);
            this.jvcApi.payload = this.payload;
            this.lastPage = lastPage.lastPage;

            this.connectedUser = this.getConnectedUser(lastDoc);

            document.querySelector('#jvchat-topic-title').textContent = lastPage.title;
            document.querySelector('#jvchat-connected-count').textContent = `${this.payload.forumInfo.header.btnVal} connecté${this.payload.forumInfo.header.btnVal === 1 ? '' : '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 = 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;
            document.querySelector('#jvchat-connected-count').textContent = `${this.payload.forumInfo.header.btnVal} connecté${this.payload.forumInfo.header.btnVal === 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">   <!-- ======== MODIF PERSO N°2 : href="${message.profileUrl}" / href="/forums/message/${message.id}" : cliquer sur pseudo = redirige vers page du message ======== -->
                            <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 = Array.from(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 = Array.from(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');
            });
        }

        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 {
                // --- ÉTAPE DE FIX POUR LES IMAGES SUPPRIMÉES ---
                // On crée un clone pour ne pas modifier l'affichage réel
                const tempDiv = messageElement.querySelector('.jvchat-content').cloneNode(true) ;

                // On transforme les JvCare en vrais liens (<a>) dans ce clone
                const jvcares = Array.from(tempDiv.getElementsByClassName("JvCare")) ;
                for (const jvcare of jvcares) {
                    const a = document.createElement("a") ;
                    const href = this.jvcClient.jvCake(jvcare.getAttribute("class")) ;
                    a.setAttribute("href", href) ;
                    // On s'assure que le contenu (l'image img-url) est préservé
                    a.innerHTML = jvcare.innerHTML ;
                    jvcare.replaceWith(a) ;
                }
                // ----------------------------------------------
                // Maintenant on utilise tempDiv au lieu du sélecteur direct
                const txt = reverseMessage(tempDiv, 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>

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

                    <div class="refresh-loader" style="display: none;--value:0;">   <!-- ======== MODIF PERSO N°1, ENLEVER LE CERCLE DE CHARGEMENT EN BAS À GAUCHE : "display: none;" ======== -->
                        <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 url = `https://www.jeuxvideo.com${deleteUrl}&ids=${message.id}`;
        //const url = `https://www.jeuxvideo.com/forums/message/delete?type=delete&ajax_hash=${deleteHash}&ids=${message.id}`;
        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();
    }

    /* GESTIONNAIRE DE LA MODIFICATION DE MESSAGE */
    async editMessage(message, messageElement) {
        try {
            const messageContent = messageElement.querySelector('.jvchat-content') ; // Au lieu d'appeler l'API qui plante, on prend le texte déjà affiché

            // --- FIX JVCARE POUR L'EDITION ---
            const tempDiv = messageContent.cloneNode(true);
            const jvcares = Array.from(tempDiv.getElementsByClassName("JvCare"));
            for (const jvcare of jvcares) {
                const a = document.createElement("a");
                const href = this.jvcClient.jvCake(jvcare.getAttribute("class"));
                a.setAttribute("href", href);
                a.innerHTML = jvcare.innerHTML;
                jvcare.replaceWith(a);
            }
            // ---------------------------------

            let currentText = message.content_raw || reverseMessage(tempDiv, true) ; // On utilise la fonction reverseMessage déjà présente dans le script pour transformer le HTML enrichi en texte brut

            // Si currentText vient de reverseMessage et contient les "> " de citation : nettoyage
            if (!message.content_raw) {
                currentText = currentText
                    .split('\n')                                                                                                  // On coupe par ligne
                    .map(line => line.startsWith('> ') ? line.substring(2) : (line.startsWith('>') ? line.substring(1) : line))   // On vire le "> " ou ">"
                    .join('\n') ;                                                                                                 // On recolle tout
            }

            const oldMessageContent = messageContent.innerHTML ;
            messageContent.innerHTML = `
                <div class="messageEditor__containerEdit">
                    <textarea class="messageEditor__edit mb-2" style="width:100%; min-height:100px; background:#222; color:#fff; border:1px solid #444;"></textarea>
                </div>
                <div class="d-flex justify-content-start gap-2">
                    <button class="simpleButton cancelMessage">Annuler</button>
                    <button class="simpleButton postMessage">Modifier</button>
                </div>
            ` ;

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

            // BOUTON ANNULER
            messageContent.querySelector('.cancelMessage').onclick = () => {
                messageContent.innerHTML = oldMessageContent ;
            } ;

            // BOUTON VALIDER
            messageContent.querySelector('.postMessage').onclick = async () => {
                try {
                    const newHtml = await this.jvcApi.updateMessage(message.id, textarea.value) ;
                    messageContent.innerHTML = newHtml ;
                    // On met à jour le content_raw pour la prochaine édition
                    message.content_raw = textarea.value ;
                } catch (e) {
                    console.error("Erreur lors de la modif : " + e.message) ;
                    messageContent.innerHTML = oldMessageContent ;
                }
            } ;
        } catch (error) {
            console.error("Erreur édit : ", error) ;
        }
    }
    /* FIN ASYNC EDITMESSAGE - GESTIONNAIRE DE LA MODIFICATION DE 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 };
}

function getPayload(doc) {
    const scripts = doc.getElementsByTagName('script');
    let rawPayloadString = null;

    for (let i = 0; i < scripts.length; i++) {
        const scriptContent = scripts[i].textContent || scripts[i].innerText;

        if (scriptContent) {
            const match = scriptContent.match(/window\.jvc\.forumsAppPayload\s*=\s*['"]([^'"]+)['"]/);
            if (match && match[1]) {
                rawPayloadString = match[1];
                break;
            }

            const jvcVarMatch = scriptContent.match(/jvc\.forumsAppPayload\s*=\s*['"]([^'"]+)['"]/);
            if (!rawPayloadString && jvcVarMatch && jvcVarMatch[1]) {
                rawPayloadString = jvcVarMatch[1];
                break;
            }
        }
    }

    if (rawPayloadString) {
        try {
            const decodedPayload = JSON.parse(atob(rawPayloadString));
            return decodedPayload;
        } catch (e) {
            err('getPayload décodage ❌:', e);
            return null;
        }
    } else {
        wrn('getPayload: forumsAppPayload introuvable dans le doc');
        return null;
    }
}

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": {
                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);
            }
        }
    }
}
Editor is loading...
Leave a Comment