OP Only 2026

Filtre automatiquement un topic pour ne garder que les messages de l'auteur. Version adaptée à la MAJ de jvc de 2026. Code en partie repris de https://jvscript.fr/script/jvchat-premium (JVChat Premium FORK by Rand0max).

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Advertisement:

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

Advertisement:

// ==UserScript==
// @name         OP Only 2026
// @namespace    OP Only 2026
// @author       navet-navrant
// @description  Filtre automatiquement un topic pour ne garder que les messages de l'auteur. Version adaptée à la MAJ de jvc de 2026. Code en partie repris de https://jvscript.fr/script/jvchat-premium (JVChat Premium FORK by Rand0max).
// @version      1.0.0
// @license      MIT
// @match        https://www.jeuxvideo.com/forums/42-*
// ==/UserScript==

// Délai d'attente entre le chargement de deux pages (ms)
const DELAY = 2500;

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function parseURL(url) {
    const regex = /^(.*?)(\/\d+-\d+-\d+-)(\d+)(-\d+-\d+-\d+-)(.*?)(\.htm)(.*)$/i;
    const [_, domain, ids, page, nums, title, htm, anchor] = url.match(regex);

    return { domain: domain, ids: ids, page: page, nums: nums, title: title, htm: htm, anchor: anchor };
}

function buildURL(dict) {
    return `${dict.domain}${dict.ids}${dict.page}${dict.nums}${dict.title}${dict.htm}${dict.anchor}`;
}

function jvCake(cls) {
    const base16 = '0A12B34C56D78E9F';
    const s = cls.split(' ')[1];
    let link = '';

    for (let i = 0; i < s.length; i += 2) {
        link += String.fromCharCode(base16.indexOf(s.charAt(i)) * 16 + base16.indexOf(s.charAt(i + 1)));
    }

    return link;
}

function decodeJvCare(doc) {
    const jvcares = doc.getElementsByClassName('JvCare');

    for (let i = jvcares.length - 1; i >= 0; i--) {
        const jvcare = jvcares[i];
        const a = doc.createElement('a');
        a.setAttribute('target', '_blank');
        a.setAttribute('href', jvCake(jvcare.getAttribute('class')));
        a.innerHTML = jvcare.innerHTML;
        const otherClasses = jvcare.getAttribute('class').split(' ').slice(2).join(' ');
        if (otherClasses) a.classList.add(...otherClasses.split(' ').filter(Boolean));
        jvcare.replaceWith(a);
    }
}

function getOPPseudo(doc) {
    return doc.querySelector('#listMessages .messageUser__label')?.textContent.trim();
}

function getLastPage(document) {
    const spans = document.querySelectorAll('.pagination__item, .pagination__button, .pagination__navigation a, .pagination__navigation span');
    let lastPage = 1;

    for (let span of spans) {
        const txt = span.textContent.trim();
        if (/^\d+$/.test(txt)) {
            const page = parseInt(txt);
            if (page > lastPage) {
                lastPage = page;
            }
        }
    }

    return lastPage;
}

async function fetchPage(dict) {
    const res = await fetch(buildURL(dict));
    const html = await res.text();
    const doc = new DOMParser().parseFromString(html, 'text/html');
    decodeJvCare(doc);
    return doc;
}

function expandNestedQuotes(msgEl) {
    msgEl.querySelectorAll('.message__blockquote').forEach(bq => {
        bq.classList.add('message__blockquote--visible');
    });
    msgEl.querySelectorAll('.message__collapsedQuote').forEach(btn => {
        btn.remove();
    });
}

function addMessageLink(msgEl, pageDict) {
    const msgId = msgEl.id?.replace('message-', '');
    if (!msgId) return;

    const anchoredDict = { ...pageDict, anchor: `#post_${msgId}` };
    const messageURL = buildURL(anchoredDict);

    const linkIcon = document.createElement('a');
    linkIcon.href = messageURL;
    linkIcon.target = '_blank';
    linkIcon.rel = 'noopener noreferrer';
    linkIcon.title = 'Voir le message dans son contexte';
    linkIcon.className = 'oponly-message-link';
    linkIcon.textContent = '↗';

    const card = msgEl.querySelector('.messageUser__card') || msgEl;
    card.appendChild(linkIcon);
}

function restoreLinks(msgEl) {
    msgEl.querySelectorAll('a').forEach(a => {
        if (a.classList.contains('messageUser__label') ||
            a.classList.contains('messageUser__date') ||
            a.classList.contains('messageUser__dateEditLink') ||
            a.classList.contains('avatar')) {
            return;
        }

        a.style.pointerEvents = 'auto';
        a.style.cursor = 'pointer';
        if (!a.querySelector('img')) {
            a.style.color = '#3690e0';
        }
    });
}

function addOPMessages(doc, OP, container, pageDict) {
    let count = 0;
    doc.querySelectorAll('#listMessages .messageUser').forEach(msg => {
        const pseudo = msg.querySelector('.messageUser__label')?.textContent.trim();
        if (pseudo === OP) {
            const clone = msg.cloneNode(true);
            clone.querySelector('.messageUser__headerActions')?.remove();
            expandNestedQuotes(clone);
            restoreLinks(clone);
            addMessageLink(clone, pageDict);
            container.appendChild(clone);
            count++;
        }
    });
    return count;
}

function showStatus(text, loading = true) {
    const paginationTop = document.querySelector('.js-pagination-top');
    const paginationBot = document.querySelector('.container__pagination:not(.js-pagination-top)');

    let statusEl = document.querySelector('#oponly-status');
    if (!statusEl) {
        if (paginationTop) paginationTop.style.display = 'none';
        if (paginationBot) paginationBot.style.display = 'none';

        const container = document.querySelector('#listMessages');
        if (!container) return;
        container.insertAdjacentHTML('beforebegin', `<div id='oponly-status' style='padding: 0.5rem 1rem; font-weight: bold;'></div>`);
        statusEl = document.querySelector('#oponly-status');
    }

    statusEl.innerHTML = loading
        ? `<span class='oponly-spinner'></span><span>${text}</span>`
        : `<span>${text}</span>`;
}

async function triggerOpOnly(event) {
    const clickedBtn = event.currentTarget;
    clickedBtn.classList.add('btn-oponly--activated');
    clickedBtn.disabled = true;

    try {
        let topicUrl = document.URL;
        let urlToFetch = parseURL(topicUrl);
        urlToFetch.page = 1;
        urlToFetch.anchor = '';

        const container = document.querySelector('#listMessages');
        if (!container) return;
        container.innerHTML = '';

        showStatus('Chargement page 1...');
        const firstPageDoc = await fetchPage(urlToFetch);
        const OP = getOPPseudo(firstPageDoc);
        if (!OP) {
            showStatus('Impossible de récupérer l\'auteur du topic.', false);
            clickedBtn.classList.remove('btn-oponly--activated');
            clickedBtn.disabled = false;
            return;
        }
        const lastPage = getLastPage(firstPageDoc);

        let opMessageCount = addOPMessages(firstPageDoc, OP, container, urlToFetch);
        showStatus(`1 sur ${lastPage} pages chargées - ${opMessageCount} messages de l'OP récupérés`, lastPage > 1);

        for (let page = 2; page <= lastPage; page++) {
            await sleep(DELAY);
            urlToFetch.page = page;
            const doc = await fetchPage(urlToFetch);
            opMessageCount += addOPMessages(doc, OP, container, urlToFetch);
            const isLast = page === lastPage;
            showStatus(`${page} sur ${lastPage} pages chargées - ${opMessageCount} messages de l'OP récupérés`, !isLast);
        }
    } catch (e) {
        alert(`OPOnly error : ${e.message}`);
        console.error('OPOnly error : ', e.message);
        clickedBtn.classList.remove('btn-oponly--activated');
        clickedBtn.disabled = false;
    }
}

function initializeButton() {
    let css = `<style type='text/css'>
    .btn-oponly.buttonsNavbar__button {
        font-weight: bold;
    }
    .btn-oponly.buttonsNavbar__button.btn-oponly--activated {
        color: #f66031;
        cursor: not-allowed;
    }
    #oponly-status {
        display: flex;
        align-items: center;
        gap: 0.5rem;
    }
    .oponly-spinner {
        width: 14px;
        height: 14px;
        border: 2px solid #ccc;
        border-top-color: #ff6333;
        border-radius: 50%;
        animation: oponly-spin 0.7s linear infinite;
    }
    @keyframes oponly-spin {
        to { transform: rotate(360deg); }
    }
    .messageUser__card {
        position: relative;
    }
    .oponly-message-link {
        position: absolute;
        top: 1rem;
        right: 1rem;
        text-decoration: none;
        cursor: pointer;
        font-size: 1.5rem;
        opacity: 0.6;
    }
    .oponly-message-link:hover {
        opacity: 1;
    }
    @media (min-width: 1000px) and (max-width: 1199px) {
        .buttonsNavbar {
            padding-left: 0;
            padding-right: 0;
        }
        .icon-refresh + .buttonsNavbar__label {
            display : none;
        }
    }
    </style>`

    document.head.insertAdjacentHTML('beforeend', css);
    const opOnlyButton = `<span class='buttonsNavbar__space'></span><button class='buttonsNavbar__button btn-oponly' type='button'><i class='buttonsNavbar__icon icon-comments'></i><div class='buttonsNavbar__label'>OP</div></button>`;
    const buttonsNavbar = document.querySelectorAll('.buttonsNavbar');

    for (let bloc of buttonsNavbar) {
        const refreshBtn = bloc.querySelector('.icon-refresh')?.closest('button');
        refreshBtn.insertAdjacentHTML('afterend', opOnlyButton);
    }

    let buttons = document.getElementsByClassName('btn-oponly');
    for (let btn of buttons) {
        btn.addEventListener('click', triggerOpOnly);
    }
}

function main() {
    if (document.querySelector('.buttonsNavbar')) {
        initializeButton();
        return;
    }

    let observer = new MutationObserver(function (mutations, obs) {
        if (document.querySelector('.buttonsNavbar')) {
            obs.disconnect();
            initializeButton();
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

    setTimeout(() => observer.disconnect(), 5000);
}

main();