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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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