9GAG Likes Page Filter Tool

A tool which allows to filter the 9GAG upvotes / likes personal page only on the tags you are interested into (e.g. funny, gaming, pcmr, etc...).

// ==UserScript==
// @name         9GAG Likes Page Filter Tool
// @namespace    [email protected]
// @version      0.2
// @description  A tool which allows to filter the 9GAG upvotes / likes personal page only on the tags you are interested into (e.g. funny, gaming, pcmr, etc...).
// @author       Pierpaolo Tommasi
// @match        *://9gag.com/u/*/likes
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        none
// @license MIT
// ==/UserScript==

// This is a set containg the tags currently filtered on
let activeFilters;
// A map where the key is the tag and the value is an array of article ID (strings)
let tagToArticles;
// A map where the key is the article ID and the value is the array of tags (strings)
let articleToTags;
// The count hide option (how many articles should a tag have in order to be displayed)
let hidesCount;
// Which sorting should be applied on the discovered tags
let sorting;

// Query the current view and return a map where the key are the tags and
// the values are the articles with the given tag.
function getNewArticles() {

    const _articleToTags = { };

    const listView = document.getElementById("list-view-2");
    const articles = [ ... listView.querySelectorAll("article") ];

    articles.forEach(a => {
        const articleId = a.getAttribute("id");
        if (!articleToTags.has(articleId)) {
            _articleToTags[articleId] = [ ... a.querySelectorAll(".ui-post-tags a") ].map(a => a.innerText);
        }
    });

    return _articleToTags;

}


// Remove the existing menu, to repaint a new one
function cleanMenu() {
    const menu = document.getElementById("tags_extra_menu");
    menu && menu.remove();
}

function createFilteredTagButton(tag, count) {
    const tagElement = document.createElement("div");
    tagElement.innerHTML = `${tag} (${count})`;
    tagElement.setAttribute("style", "display: inline-block; margin: 2px 4px; padding: 4px 2px 4px 4px; background-color: powderblue; border-radius: 2px;");
    const addElement = document.createElement("span");
    addElement.innerHTML = `Clear`;
    addElement.setAttribute("style", "cursor: pointer; margin: 2px 0px 2px 8px; padding: 2px; background-color: mediumslateblue; border-radius: 2px;");
    tagElement.appendChild(addElement);
    addElement.onclick = () => {
        if(activeFilters.has(tag)) {
            activeFilters.delete(tag);
            repaintMenu("clear link -> onclick");
            updateFilters();
        }
    };
    return tagElement;
}

function createTagButton(tag, count) {
    const tagElement = document.createElement("div");
    tagElement.innerHTML = `${tag} (${count})`;
    tagElement.setAttribute("style", "display: inline-block; margin: 2px 4px; padding: 4px 2px 4px 4px; background-color: powderblue; border-radius: 2px;");
    const addElement = document.createElement("span");
    addElement.innerHTML = `Add`;
    addElement.setAttribute("style", "cursor: pointer; margin: 2px 0px 2px 8px; padding: 2px; background-color: mediumslateblue; border-radius: 2px;");
    tagElement.appendChild(addElement);
    addElement.onclick = () => {
        if(!activeFilters.has(tag)) {
            activeFilters.add(tag);
            repaintMenu("add link -> onclick");
            updateFilters();
        }
    };
    return tagElement;
}

function createFiltersMenu() {

    const filtersMenu = document.createElement("div");

    const filtersLabel = document.createElement("div");
    filtersLabel.innerHTML = "Only posts with one of these tags will be displayed:";
    filtersMenu.appendChild(filtersLabel);

    if (activeFilters.size > 0) {
        activeFilters.forEach(tag => {
            filtersMenu.appendChild(createFilteredTagButton(tag, tagToArticles.get(tag).length));
        });
    } else {
        const noFilters = document.createElement("div");
        noFilters.innerHTML = "No selection yet.";
        filtersMenu.appendChild(noFilters);
    }

    return filtersMenu;

}

function createTagOptionsMenu() {

    const tagOptionsMenu = document.createElement("div");

    //------ Sub menu with the tags count

    const countLabelSubMenu = document.createElement("div");

    const countLabelBefore = document.createElement("span");
    countLabelBefore.innerHTML = "Only tags with at least ";

    const countLabelInput = document.createElement("input");
    countLabelInput.setAttribute("type", "number");
    countLabelInput.setAttribute("min", "1");
    countLabelInput.setAttribute("max", "99");
    countLabelInput.setAttribute("value", hidesCount);
    countLabelInput.onchange = () => {
        const newHidesCount = parseInt(countLabelInput.value);
        if (!isNaN(newHidesCount)) {
            hidesCount = newHidesCount;
            repaintMenu("input hides count -> onchange");
        }
    };

    const countLabelAfter = document.createElement("span");
    countLabelAfter.innerHTML = " posts will be displayed.";

    countLabelSubMenu.appendChild(countLabelBefore);
    countLabelSubMenu.appendChild(countLabelInput);
    countLabelSubMenu.appendChild(countLabelAfter);

    tagOptionsMenu.appendChild(countLabelSubMenu);

    //------ Sub menu with the sorting

    const sortTagSubMenu = document.createElement("div");
    sortTagSubMenu.setAttribute("style", "margin-top: 3px;");

    const sortLabel = document.createElement("span");
    sortLabel.innerHTML = "Sort tags by ";
    const sortInput = document.createElement("select");
    sortInput.setAttribute("style", "height: inherit; padding: inherit; font-weight: normal; margin: 0; display: inline-block;");

    const unsortedOpt = document.createElement("option");
    unsortedOpt.innerHTML = "Unsorted";
    unsortedOpt.setAttribute("value", "unsorted");
    if (sorting === "unsorted") {
        unsortedOpt.setAttribute("selected", "true");
    }
    sortInput.appendChild(unsortedOpt);

    const articlesCountOpt = document.createElement("option");
    articlesCountOpt.innerHTML = "Articles count";
    articlesCountOpt.setAttribute("value", "count");
    if (sorting === "count") {
        articlesCountOpt.setAttribute("selected", "true");
    }
    sortInput.appendChild(articlesCountOpt);

    const alphabeticOpt = document.createElement("option");
    alphabeticOpt.innerHTML = "Alphabetic order";
    alphabeticOpt.setAttribute("value", "alphabetic");
    if (sorting === "alphabetic") {
        alphabeticOpt.setAttribute("selected", "true");
    }
    sortInput.appendChild(alphabeticOpt);

    sortInput.onchange = () => {
        sorting = sortInput.value;
        repaintMenu("select sorting -> onchange");
    };

    sortTagSubMenu.appendChild(sortLabel);
    sortTagSubMenu.appendChild(sortInput);

    tagOptionsMenu.appendChild(sortTagSubMenu);

    //------ Sub menu for custom tags

    const manualAddSubMenu = document.createElement("div");
    manualAddSubMenu.setAttribute("style", "margin-top: 3px;");

    const addLabel = document.createElement("span");
    addLabel.innerHTML = "Manually add a tag: ";

    const addTextInput = document.createElement("input");
    addTextInput.setAttribute("type", "text");
    addTextInput.setAttribute("style", "height: inherit; padding: inherit; display: inline; margin: inherit; width: 60px;");
    const addButton = document.createElement("button");
    addButton.innerHTML = "Add";

    addButton.onclick = () => {
        if (addTextInput.value) {
            activeFilters.add(addTextInput.value);
            if(!tagToArticles.has(addTextInput.value)) {
                tagToArticles.set(addTextInput.value, []);
            }
            repaintMenu("add tag -> onclick");
            updateFilters();
        }
    }

    manualAddSubMenu.appendChild(addLabel);
    manualAddSubMenu.appendChild(addTextInput);
    manualAddSubMenu.appendChild(addButton);

    tagOptionsMenu.appendChild(manualAddSubMenu);

    //------

    return tagOptionsMenu;

}

function createTagsMenu() {

    const tagsMenu = document.createElement("div");

    const tagLabel = document.createElement("div");
    tagLabel.innerHTML = "Tags discovered so far:";
    tagsMenu.appendChild(tagLabel);

    const getSortedTags = () => {
        const tags = [ ... tagToArticles.keys() ];
        switch(sorting) {
            case "unsorted":
                return tags;
            case "alphabetic":
                return tags.sort((t1, t2) => t1.toLocaleLowerCase().localeCompare(t2.toLocaleLowerCase()));
            case "count":
                return tags.sort((t1, t2) => tagToArticles.get(t2).length - tagToArticles.get(t1).length);
            default:
                return tags;
        }
    }

    getSortedTags().forEach(tag => {
        const count = tagToArticles.get(tag).length;
        if (count >= hidesCount) {
            tagsMenu.appendChild(createTagButton(tag, count));
        }
    });

    return tagsMenu;

}

function repaintMenu(caller) {

    cleanMenu();

    // console.log("Repainting menu after call from ", caller);

    const menu = document.createElement("div");
    menu.setAttribute("id", "tags_extra_menu");
    menu.setAttribute("style", "position: fixed; right: calc(var(--main-margin) - 60px); top: 42px; bottom: 0px; overflow-y: scroll; width: 360px; margin: 10px; padding: 6px; background-color: white; z-index: 100;");
    document.querySelector("#page").before(menu);

    menu.appendChild(createFiltersMenu());
    menu.appendChild(document.createElement("hr"));
    menu.appendChild(createTagOptionsMenu());
    menu.appendChild(document.createElement("hr"));
    menu.appendChild(createTagsMenu());

}

function shouldHide(articleId) {
    if (activeFilters.size == 0) {
        return false;
    }
    const tags = articleToTags.get(articleId);
    let hide = true;
    tags.forEach(t => {
        if(activeFilters.has(t)) {
            hide = false;
        }
    });
    return hide;
}

function updateFilters(delta) {

    const updateArticle = (articleId) => {
        const el = document.getElementById(articleId);
        if (shouldHide(articleId)) {
            el.setAttribute("style", "display: none");
        } else {
            el.removeAttribute("style");
        }
    }

    if (delta == undefined) {
        for (const articleId of articleToTags.keys()) {
            updateArticle(articleId);
        }
    } else {
        for (const articleId in delta) {
            updateArticle(articleId);
        }
    }

}

(function() {
    'use strict';

    console.log("9gag like filter activated!");

    const addDelta = (newArticleToTags) => {

        for (const articleId in newArticleToTags) {
            if (!articleToTags.has(articleId)) {
                const tags = newArticleToTags[articleId];
                articleToTags.set(articleId, tags);
                tags.forEach(tag => {
                    if(tagToArticles.has(tag)) {
                        tagToArticles.get(tag).push(articleId);
                    } else {
                        tagToArticles.set(tag, [articleId]);
                    }
                });
            }
        }

        repaintMenu("addDelta");
        updateFilters(newArticleToTags);

    }

    // 9gag.com/u/<anything>/likes/
    const likePage = /9gag\.com\/u\/\w*\/likes/;

    let isFilterActive = false;

    setInterval(() => {

        if (likePage.test(window.location.href)) {
            if (!isFilterActive) {
                console.log("Activating tag filter menu in /likes page");
                isFilterActive = true;
                activeFilters = new Set();
                tagToArticles = new Map();
                articleToTags = new Map();
                hidesCount = 1;
                sorting = "unsorted";
            }
        } else {
            if (isFilterActive) {
                console.log("De-activating tag filter menu in /likes page");
                isFilterActive = false;
                cleanMenu();
            }
        }

        if (isFilterActive) {
            const newArticleToTags = getNewArticles();
            if (Object.keys(newArticleToTags).length > 0) {
                addDelta(newArticleToTags);
            }
        }

    }, 500);

})();