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).

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Advertisement:

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

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