ForumLive

Outil pour faciliter l'actualisation de la liste des topics sur les forums de Jeuxvideo.com

// ==UserScript==
// @name         ForumLive
// @namespace    ForumLive
// @version      0.1.7
// @license      MIT
// @description  Outil pour faciliter l'actualisation de la liste des topics sur les forums de Jeuxvideo.com
// @author       Me
// @match        http://*.jeuxvideo.com/forums/0-*
// @match        https://*.jeuxvideo.com/forums/0-*
// @grant        none
// ==/UserScript==

let forumUrl = undefined;
let initialized = false;
let intervalId = undefined;
let timeoutId = undefined;
let displayedPage = 0;
let currentMode = "classic";
let currentConnected = undefined;
let nbMps = undefined;
let nbNotifs = undefined;
let currentlyDisplayedTopics = undefined;
let dataTopics = {};
let requestsCounter = 0;
let topicsDOM = [];
let topicsLength = undefined;
let isOver = false;

function addCss() {
    let rules = `
        #forum-live-mode {
        }

        #forum-live-button {
            min-width: 4.5rem;
            margin-left: -75px;
        }

        .forum-live-activated .bloc-pagi-default {
            height: 28px;
        }

        .forum-live-activated .forum-live-tempo {
            border-color: blue;
        }

        .forum-live-activated .conteneur-topic-pagi:hover .topic-list {
            border-color: red;
        }

        .forum-live-hide {
            display: none!important;
        }

        .forum-live-hide-pagi-before .pagi-debut-actif,
        .forum-live-hide-pagi-before .pagi-precedent-actif {
            display: none;
        }

        .forum-live-hide-pagi-after .pagi-suivant-actif {
            display: none;
        }

        .forum-live-activated #forum-live-button {
            border: 0.0625rem solid #c28507;
            background: #f0a100;
            color: #fff;
        }

        .forum-live-new-topic-1 {
            animation-duration:0.8s;
            animation-name: slidein-1;
        }

        .forum-live-new-topic-2 {
            animation-duration:0.8s;
            animation-name: slidein-2;
        }

        @keyframes slidein-1 {
            from {
              opacity: 0.1;
            }
            to {
              opacity: 1;
            }
        }

        @keyframes slidein-2 {
            from {
              opacity: 0.1;
            }
            to {
              opacity: 1;
            }
        }
    `;
    let css = `<style type="text/css" id="forum-live-css">${rules}</style>`;
    document.head.insertAdjacentHTML("beforeend", css);
}

function normalizeForumURL(url) {
    let regex = /^.*?\/\d+-(\d+)-\d+-\d+-\d+-\d+-\d+-(.*?)\.htm.*$/i;
    let [_, num, name] = url.match(regex);
    return `https://www.jeuxvideo.com/forums/0-${num}-0-1-0-1-0-${name}.htm`;
};

function process(dom, init) {
    requestsCounter++;
    let results = parseForum(dom);
    populateTopics(results);

    if (init) {
        display(currentMode, false, true);
        return;
    }

    if (isOver) {
        return;
    }

    let topicListClasses = document.getElementsByClassName("topic-list")[0].classList;
    topicListClasses.add("forum-live-tempo");

    timeoutId = setTimeout(function () {
        topicListClasses.remove("forum-live-tempo");
        display(currentMode, true, true);
    }, 1000);

};

function updateForum() {
    // Ensure that if "Live" button is clicked/unclicked, older request does not conflict with new one
    let currentId = intervalId;

    request(forumUrl, function (response) {
        if (currentId !== intervalId) {
            return;
        }
        process(response, false);
    });
};

function request(url, callback) {
    let xhr = new XMLHttpRequest();

    xhr.ontimeout = function () {
        console.error(`La délai d'attente de la requête a expiré`);
    };

    xhr.onerror = function () {
        console.error(`La requête a échoué (${xhr.status}): ${xhr.statusText}`);
    };

    xhr.onabort = function () {
        console.error(`La requête a été interrompue pour une raison inconnue`);
    };

    xhr.onload = function () {
        if (xhr.status !== 200) {
            console.error(`La requête a retourné une erreur (${xhr.status}): ${xhr.statusText}`);
            return;
        }
        callback(xhr.response);
    };

    xhr.responseType = "document";
    xhr.timeout = 5000;

    xhr.open("GET", url, true);
    xhr.setRequestHeader("Cache-Control", "no-cache, no-store, must-revalidate");
    xhr.send();
};

function parseForum(document) {
    let connected = document.getElementsByClassName("nb-connect-fofo")[0];
    if (connected) {
        connected = parseInt(connected.textContent.trim());
    }
    let mps = document.getElementsByClassName("headerAccount__pm")[0];
    if (mps) {
        mps = parseInt(mps.getAttribute("data-val"));
    }
    let notifs = document.getElementsByClassName("headerAccount__notif")[0];
    if (notifs) {
        notifs = parseInt(notifs.getAttribute("data-val"));
    }
    let topicList = document.getElementsByClassName("topic-list")[0];
    let topics = [];
    for (let li of topicList.children) {
        if (!li.hasAttribute("data-id")) {
            continue;
        }
        let dataId = li.getAttribute("data-id");
        let topicImg = li.getElementsByClassName("topic-img")[0];
        let type = topicImg.title;
        let iconClasses = topicImg.className;
        let topicTitle = li.getElementsByClassName("topic-title")[0];
        let title = topicTitle.title;
        let href = topicTitle.href;
        let topicAuthor = li.getElementsByClassName("topic-author")[0];
        let author = topicAuthor.textContent.trim();
        let authorClass = topicAuthor.className;
        let count = parseInt(li.getElementsByClassName("topic-count")[0].textContent.trim());
        let date = li.getElementsByClassName("topic-date")[0].textContent.trim();
        topics.push({ type: type, iconClasses: iconClasses, title: title, href: href, author: author, authorClass: authorClass, count: count, date: date, dataId: dataId });
    }
    return { connected: connected, topics: topics, mps: mps, notifs: notifs };
};

function update(element, topic) {
    let authorLink;
    if (topic.author === "Pseudo supprimé") {
        authorLink = `<span class="topic-author" style="font-style: italic;">Pseudo supprimé</span>`;
    } else {
        authorLink = `<a href="https://www.jeuxvideo.com/profil/${topic.author.toLowerCase()}?mode=infos" target="_blank" class="${topic.authorClass}">${topic.author}</a>`;
    }

    let lastPage = parseInt(topic.count / 20) + 1;
    let urlLastPage = topic.href.replace("-1-0-1-0-", `-${lastPage}-0-1-0-`);

    element.setAttribute("data-id", topic.dataId);
    let topicImg = element.getElementsByClassName("topic-img")[0];
    topicImg.className = topic.iconClasses;
    topicImg.alt = topic.type;
    topicImg.title = topic.type;
    let topicTitle = element.getElementsByClassName("topic-title")[0];
    topicTitle.href = topic.href;
    topicTitle.title = topic.title;
    topicTitle.innerHTML = topic.title;
    element.getElementsByClassName("topic-author")[0].outerHTML = authorLink;
    element.getElementsByClassName("topic-count")[0].innerHTML = topic.count;
    let date = element.getElementsByClassName("topic-date")[0].getElementsByTagName("a")[0];
    date.href = urlLastPage;
    date.innerHTML = topic.date;
};

function populateTopics(results) {
    currentConnected = results.connected;
    nbMps = results.mps;
    nbNotifs = results.notifs;
    let topicsListIndex = 0;

    for (let topic of results.topics) {
        if (!dataTopics.hasOwnProperty(topic.dataId)) {
            let creationIndex = -1;
            if (topic.count <= 1) {
                creationIndex = requestsCounter + 1.0 / (topicsListIndex + 2);
            }
            dataTopic = { topic: topic, counts: [[requestsCounter, topic.count]], requestsCounter: requestsCounter, topicsListIndex: topicsListIndex, creationIndex: creationIndex, isNew: true };
            dataTopics[topic.dataId] = dataTopic;
        } else {
            dataTopic = dataTopics[topic.dataId];
            while (dataTopic.counts.length > 1 && requestsCounter - dataTopic.counts[0][0] > 60) {
                dataTopic.counts.shift();
            }
            dataTopic.isNew = false;
            dataTopic.topic = topic;
            dataTopic.counts.push([requestsCounter, topic.count]);
            dataTopic.requestsCounter = requestsCounter;
            dataTopic.topicsListIndex = topicsListIndex;
        }

        topicsListIndex++;
    }
};

function compareClassic(a, b) {
    if (a.requestsCounter > b.requestsCounter) {
        return -1;
    }
    if (a.requestsCounter < b.requestsCounter) {
        return 1;
    }
    if (a.topicsListIndex < b.topicsListIndex) {
        return -1;
    }
    if (a.topicsListIndex > b.topicsListIndex) {
        return 1;
    }
    return 0;
};

function compareNew(a, b) {
    if (a.creationIndex > b.creationIndex) {
        return -1;
    }
    if (a.creationIndex < b.creationIndex) {
        return 1;
    }
    if (a.topic.dataId > b.topic.dataId) {
        return -1;
    }
    if (a.topic.dataId < b.topic.dataId) {
        return 1;
    }
    return 0;
};

function compareHot(a, b) {
    let lenA = a.counts.length;
    let lenB = b.counts.length;

    let minA = a.counts[lenA - 1][1];
    let minB = b.counts[lenB - 1][1];
    let maxA = minA;
    let maxB = minB;

    for (let i = lenA - 2; i >= 0; i--) {
        let count = a.counts[i];
        if (requestsCounter - count[0] > 60) {
            break;
        }
        if (count[1] < minA) {
            minA = count[1];
        } else if (count[1] > maxA) {
            maxA = count[1];
        }
    }

    for (let i = lenB - 2; i >= 0; i--) {
        let count = b.counts[i];
        if (requestsCounter - count[0] > 60) {
            break;
        }
        if (count[1] < minB) {
            minB = count[1];
        } else if (count[1] > maxB) {
            maxB = count[1];
        }
    }

    let diffA = maxA - minA;
    let diffB = maxB - minB;


    if (diffA > diffB) {
        return -1;
    }
    if (diffB > diffA) {
        return 1;
    }

    let lastA = a.counts[lenA - 1][0];
    let lastB = b.counts[lenB - 1][0];

    if (lastA > lastB) {
        return -1;
    }
    if (lastB > lastA) {
        return 1;
    }

    if (a.topic.count > b.topic.count) {
        return -1;
    }
    if (b.topic.count > a.topic.count) {
        return 1;
    }
    return 0;
};

function slideIn(elem) {
    if (elem.classList.contains("forum-live-new-topic-1")) {
        elem.classList.remove("forum-live-new-topic-1");
        elem.classList.add("forum-live-new-topic-2");
    } else {
        elem.classList.remove("forum-live-new-topic-2");
        elem.classList.add("forum-live-new-topic-1");
    }
};

function display(mode, slidingIn, refresh) {
    document.getElementsByClassName("nb-connect-fofo")[0].innerHTML = `${currentConnected} connecté(s)`;

    if (nbMps !== undefined) {
        let mps = document.getElementsByClassName("headerAccount__pm")[0];
        if (mps && parseInt(mps.getAttribute("data-val")) !== nbMps) {
            if (nbMps === 0) {
                mps.classList.remove("has-notif");
            } else {
                mps.classList.add("has-notif");
            }
            mps.setAttribute("data-val", nbMps);
        }
    }

    if (nbNotifs !== undefined) {
        let notifs = document.getElementsByClassName("headerAccount__notif")[0];
        if (notifs && parseInt(notifs.getAttribute("data-val")) !== nbNotifs) {
            if (nbNotifs === 0) {
                notifs.classList.remove("has-notif");
            } else {
                notifs.classList.add("has-notif");
            }
            notifs.setAttribute("data-val", nbNotifs);
        }
    }

    let topics;

    if (refresh) {
        topics = Object.values(dataTopics);
        currentlyDisplayedTopics = JSON.parse(JSON.stringify(topics));
    } else {
        topics = currentlyDisplayedTopics;
    }

    if (mode == "classic") {
        topics.sort(compareClassic);
    } else if (mode == "new") {
        topics.sort(compareNew);
    } else if (mode == "hot") {
        topics.sort(compareHot);
    }

    let maxPage = parseInt((topics.length - 1) / topicsLength);
    let mainClasses = document.getElementById("forum-main-col").classList;

    if (displayedPage === 0) {
        mainClasses.add("forum-live-hide-pagi-before");
    } else {
        mainClasses.remove("forum-live-hide-pagi-before");
    }

    if (displayedPage === maxPage) {
        mainClasses.add("forum-live-hide-pagi-after");
    } else {
        mainClasses.remove("forum-live-hide-pagi-after");
    }

    let displayedTopics = topics.slice(displayedPage * topicsLength, (displayedPage + 1) * topicsLength);

    let changed = false;

    for (let i = displayedTopics.length - 1; i >= 0; i--) {
        let topic = displayedTopics[i];
        let elem = topicsDOM[i];
        elem.classList.remove("forum-live-hide");
        if (slidingIn) {
            let len = topic.counts.length;
            if (mode === "classic") {
                if (!changed && (len < 2 || (topic.counts[len - 1][1] > topic.counts[len - 2][1]))) {
                    changed = true;
                }
                if (changed && requestsCounter > 1 && topic.topic.type !== "Topic épinglé") {
                    slideIn(elem)
                }
            } else if (mode === "new") {
                if (topic.isNew && topic.creationIndex !== -1) {
                    topic.isNew = false;
                    slideIn(elem);
                }
            } else if (mode === "hot") {
                if (elem.getAttribute("data-id") !== topic.topic.dataId) {
                    slideIn(elem);
                }
            }
        } else {
            elem.classList.remove("forum-live-new-topic-1");
            elem.classList.remove("forum-live-new-topic-2");
        }

        update(elem, topic.topic);
    }

    for (let i = displayedTopics.length; i < topicsLength; i++) {
        topicsDOM[i].classList.add("forum-live-hide");
    }

    if (document.activeElement.classList.contains("lien-jv")) {
        document.activeElement.blur();
    }
};

function addForumLiveButton() {
    let button = `<button id="forum-live-button" class="btn btn-actu-new-list-forum btn-actualiser-forum">Live</button>`;
    let blocPreRight = document.getElementsByClassName("bloc-pre-right")[0];
    if (!blocPreRight) {
        console.error("Could not find 'Actualiser' button");
        return;
    }
    blocPreRight.insertAdjacentHTML("afterbegin", button);
}

function addForumLiveSelect() {
    let select = `
    <div class="forum-live-hide" id="forum-live-mode">
        <select id="forum-live-select" title="Choisir le critère de tri des topics">
            <option value="classic">Classique</option>
            <option value="new">Nouveau</option>
            <option value="hot">Tendance</option>
        </select>
    </div>
    `;
    let pagiBefore = document.getElementsByClassName("pagi-before-list-topic")[0];
    if (!pagiBefore) {
        console.error("Could not find 'pagi-before-list-topic' button");
        return;
    }
    pagiBefore.insertAdjacentHTML("afterend", select);
}

function bindForumLiveSelect() {
    let select = document.getElementById("forum-live-select");
    select.addEventListener("change", changeMode);
}

function changeMode(event) {
    currentMode = event.target.value;
    display(currentMode, false, false);
};

function bindForumLiveButton() {
    let button = document.getElementById("forum-live-button");
    button.addEventListener("click", toggleForumLive);
}

function isActivated() {
    return document.getElementById("forum-main-col").classList.contains("forum-live-activated");
}

function toggleForumLive(event) {
    event.stopImmediatePropagation()

    if (!initialized) {
        initialized = true;

        // TamperMonkey / Chrome bug: https://github.com/Tampermonkey/tampermonkey/issues/705#issuecomment-493895776
        if (window) {
            if (window.clearTimeout) {
                window.clearTimeout = window.clearTimeout.bind(window);
            }
            if (window.clearInterval) {
                window.clearInterval = window.clearInterval.bind(window);
            }
            if (window.setTimeout) {
                window.setTimeout = window.setTimeout.bind(window);
            }
            if (window.setInterval) {
                window.setInterval = window.setInterval.bind(window);
            }
        }

        for (let elem of document.getElementsByClassName("topic-list")[0].children) {
            if (!elem.hasAttribute("data-id")) {
                continue;
            }
            elem.className = "";  // Remove blue overlay for deleted topic
            topicsDOM.push(elem);
        }
        topicsLength = topicsDOM.length;

        forumUrl = normalizeForumURL(document.URL);

        addForumLiveSelect();
        bindForumLiveSelect();

        document.getElementsByClassName("conteneur-topic-pagi")[0].addEventListener("mouseenter", function (event) {
            isOver = true;
            clearTimeout(timeoutId);
            document.getElementsByClassName("topic-list")[0].classList.remove("forum-live-tempo");
            timeoutId = undefined;
        })

        document.getElementsByClassName("conteneur-topic-pagi")[0].addEventListener("mouseleave", function (event) {
            isOver = false;
        });

        for (let bloc of document.getElementsByClassName("bloc-pagi-default")) {
            let pagiPrevious = bloc.getElementsByClassName("pagi-before-list-topic")[0];
            pagiPrevious.innerHTML = `<span><a href="${forumUrl}" class="pagi-debut-actif icon-back2"><span>Début</span></a></span><span><a href="${forumUrl}" class="pagi-precedent-actif icon-back"><span>Page précédente</span></a></span>`;

            let pagiNext = bloc.getElementsByClassName("pagi-before-list-topic")[1];
            pagiNext.innerHTML = `<span><a href="${forumUrl.replace("-1-0-1-0-", "-1-0-26-0-")}" class="pagi-suivant-actif icon-next4"><span>Page suivante</span></a></span>`;
        }


        function pageDebut(event) {
            if (!isActivated()) {
                return;
            }
            event.preventDefault();
            displayedPage = 0;
            display(currentMode, false, false);
        };

        function pagePrecedent(event) {
            if (!isActivated()) {
                return;
            }
            event.preventDefault();
            displayedPage--;
            display(currentMode, false, false);
        };

        function pageSuivant(event) {
            if (!isActivated()) {
                return;
            }
            event.preventDefault();
            displayedPage++;
            display(currentMode, false, false);
        }

        for (let elem of document.getElementsByClassName("pagi-debut-actif")) {
            elem.addEventListener("click", pageDebut);
        }

        for (let elem of document.getElementsByClassName("pagi-precedent-actif")) {
            elem.addEventListener("click", pagePrecedent);
        }

        for (let elem of document.getElementsByClassName("pagi-suivant-actif")) {
            elem.addEventListener("click", pageSuivant);
        }

    }

    document.getElementById("forum-main-col").classList.toggle("forum-live-activated");
    let mode = document.getElementById("forum-live-mode");
    mode.classList.toggle("forum-live-hide");

    if (isActivated()) {
        for (let topic of topicsDOM) {
            for (let link of topic.getElementsByClassName("lien-jv")) {
                link.setAttribute("target", "_blank");
            }
        }
        process(document, true);
        intervalId = setInterval(updateForum, 5000);
    } else {
        clearTimeout(intervalId);
        intervalId = undefined;
        displayedPage = 0;
        display("classic", false, true);
        dataTopics = {};
        requestsCounter = 0;
        document.getElementById("forum-main-col").classList.remove("forum-live-hide-pagi-after");
        document.getElementById("forum-main-col").classList.add("forum-live-hide-pagi-before");
        document.getElementsByClassName("topic-list")[0].classList.remove("forum-live-tempo");
        for (let topic of topicsDOM) {
            for (let link of topic.getElementsByClassName("lien-jv")) {
                link.removeAttribute("target");
            }
        }
    }
}

function main() {
    addCss();
    addForumLiveButton();
    bindForumLiveButton();
}

main();