Greasy Fork is available in English.

Reddit Chemo

Filter, block, and remove unwanted subreddit posts of your choosing and remove ads on the Reddit feed.

// ==UserScript==
// @name        Reddit Chemo
// @namespace   https://lawrenzo.com/p/reddit-chemo
// @version     2.2.0
// @description Filter, block, and remove unwanted subreddit posts of your choosing and remove ads on the Reddit feed.
// @author      Lawrence Sim
// @license     WTFPL (http://www.wtfpl.net)
// @grant       unsafeWindow
// @grant       GM_getValue
// @grant       GM.getValue
// @grant       GM_setValue
// @grant       GM.setValue
// @match       *://*.reddit.com/*
// @noframes
// ==/UserScript==
(function() {
    //-------------------------------------------------------------------------------------
    // Prepare styles
    //-------------------------------------------------------------------------------------
    let styles = {
        ".rchemo-post": {
            background:        "rgb(255 240 240)"
        },
        ".rchemo-dark .rchemo-post": {
            background:        "rgb(80 70 70)"
        },
        ".rchemo-post.rchemo-card": {
            height:            "2em"
        },
        ".rchemo-post div[data-click-id='background']": {
            background:        "none !important",
            color:             "rgb(163 149 149)",
            'font-size':       "0.7em",
            height:            "1.6em",
            'line-height':     "1.7em"
        },
        ".rchemo-post.rchemo-classic div[data-click-id='background']": {
            padding:           "0.1em 0 0.15em 0.7em"
        },
        ".rchemo-post.rchemo-compact div[data-click-id='background']": {
            padding:           "0",
            "font-size":       "0.6em"
        },
        ".rchemo-post.rchemo-card div[data-click-id='background']": {
            'border-left':     "none",
            padding:           "0.2em 0.4em",
            "font-size":       "0.95em"
        },
        ".rchemo-post button[data-click-id='downvote'] .icon": {
            top:               "2px",
            left:              "2px",
            'line-height':     "14px",
            'font-size':       "14px"
        },
        ".rchemo-post.rchemo-classic .voteButton, .rchemo-post.rchemo-classic .voteButton span": {
            width:             "46px"
        },
        ".rchemo-post.rchemo-card button[data-click-id='downvote'] .icon": {
            top:               "8px",
            left:              "2px",
            'line-height':     "20px",
            'font-size':       "20px"
        },
        ".rchemo-post.rchemo-card .voteButton, .rchemo-post.rchemo-card .voteButton span": {
            width:             "46px",
            height:            "36px"
        },
        ".rchemo-unban": {
            margin:            "0",
            'margin-left':     "0.6em",
            padding:           "0.1em 0.4em",
            background:        "rgb(135 158 200)",
            'border-radius':   "2px",
            color:             "#fff",
            'line-height':     "normal"
        },
        ".rchemo-dark .rchemo-unban": {
            background:        "rgb(145 125 125)",
            color:             "#000"
        },
        ".rchemo-post.rchemo-compact .rchemo-unban": {
            padding:           "0 0.4em",
            'line-height':     "1.2em"
        },
        ".rchemo-post.rchemo-card .rchemo-unban": {
            'font-size':      "0.85em"
        },
        ".rchemo-unban:hover": {
            background:        "rgb(185 200 220)"
        },
        ".rchemo-dark .rchemo-unban:hover": {
            background:        "rgb(205 180 180)"
        },
        ".rchemo-join, .rchemo-ban": {
            'border-radius':   "0.9em"
        },
        ".rchemo-classic .rchemo-join, .rchemo-compact .rchemo-join": {
            'min-height':      "auto",
            padding:           "0.3em 0.6em",
            'line-height':     "0.8em",
            'font-size':       "0.7em"
        },
        ".rchemo-ban": {
            margin:            "0 0.2em 0 0.4em",
            background:        "rgb(120 45 45)",
            padding:           "0.34em 1.2em",
            color:             "#fff",
            'line-height':     "1.2em",
            'font-size':       "1.06em"
        },
        ".rchemo-ban:hover": {
            background:      "rgb(180 85 85)"
        },
        ".rchemo-classic .rchemo-ban, .rchemo-compact .rchemo-ban": {
            padding:           "0.2em 0.6em",
            'line-height':     "0.8em",
            'font-size':       "0.75em"
        },
        ".rchemo-compact .rchemo-ban": {
            'font-size':       "0.85em"
        },
        ".rchemo-counter": {
            position:          "fixed",
            top:               "60px",
            right:             "25px",
            width:             "120px",
            "z-index":         "70",
            background:        "rgb(255 240 240)",
            border:            "1px solid rgb(123, 106, 109)",
            "border-radius":   "0.2em",
            padding:           "0.2em 0.4em",
            "text-align":      "center",
            "font-size":       "0.75em",
            color:             "#333"
        },
        ".rchemo-dark .rchemo-counter": {
            background:        "rgb(80 70 70)",
            border:            "1px solid rgb(123, 106, 109)",
            color:             "#ddd"
        },
        ".rchemo-counter-content": {
            "margin-top":      "0.4em",
            display:           "none"
        },
        ".rchemo-counter:hover > .rchemo-counter-content": {
            display:           "block"
        },
        ".rchemo-counter p": {
            padding:           "0",
            margin:            "0"
        },
        ".rchemo-counter button": {
            "margin-bottom":   "0.2em"
        },
        ".rchemo-btn-showhide, .rchemo-btn-editlist": {
            "margin-top":      "0.2em",
            "text-decoration": "underline",
            color:             "rgb(77, 113, 68)"
        },
        ".rchemo-dark .rchemo-btn-showhide, .rchemo-dark .rchemo-btn-editlist": {
            color:             "rgb(182, 198, 178)"
        },
        ".rchemo-btn-showhide:hover, .rchemo-btn-editlist:hover": {
            color:             "rgb(141, 187, 128) !important"
        },
        ".rchemo-btn-darkmode": {
            "margin-top":      "0.2em",
            background:        "rgb(135, 158, 200)",
            color:             "#eee",
            padding:           "0.2em 0.5em",
            "border-radius":   "0.6em",
            "border":          "1px solid #888"
        },
        ".rchemo-btn-darkmode:hover": {
            "border-color":    "#444"
        },
        ".rchemo-dark .rchemo-btn-darkmode": {
            background:        "rgb(0 0 0)",
            color:             "#aaa"
        },
        ".rchemo-dark .rchemo-btn-darkmode:hover": {
            "border-color":    "#fff"
        },
        ".rchemo-btn-support": {
            "text-decoration": "underline",
            "font-size":       "0.85em",
            color:             "rgb(90, 108, 140)"
        },
        ".rchemo-btn-support:hover": {
            color:             "rgb(135, 158, 200)"
        },
        ".rchemo-dark .rchemo-btn-support": {
            color:             "rgb(135, 158, 200)"
        },
        ".rchemo-dark .rchemo-btn-support:hover": {
            color:             "rgb(208, 225, 255)"
        },
        ".rchemo-edit-container": {
            position:          "fixed",
            top:               "50%",
            left:              "50%",
            transform:         "translate(-50%,-50%)",
            "z-index":         "90",
            width:             "240px",
            background:        "rgb(255 240 240)",
            'border-radius':   "0.2em",
            border:            "1px solid rgb(123, 106, 109)",
            padding:           "0.2em 0",
            "font-size":       "0.9em",
            color:             "#333",
            'user-select':     "none",
            cursor:            "default"
        },
        ".rchemo-dark .rchemo-edit-container": {
            background:        "rgb(80 70 70)",
            border:            "1px solid rgb(123, 106, 109)",
            color:             "#ddd"
        },
        ".rchemo-edit-container p": {
            padding:           "0 0.2em"
        },
        ".rchemo-edit-container ul": {
            height:            "240px",
            width:             "100%",
            'overflow-y':      "scroll",
            'overflow-x':      "hidden",
            background:        "#fff",
            'border-top':      "1px solid #333",
            'border-bottom':   "1px solid #333",
            "box-sizing":      "border-box",
            "list-style":      "none"
        },
        ".rchemo-dark  .rchemo-edit-container ul": {
            background:        "#222",
            'font-size':       "0.95em",
            padding:           "0 0.2em",
            'user-select':     "none",
            'border-color':    "#bbb"
        },
        ".rchemo-edit-container li": {
            position:          "relative",
            cursor:            "pointer",
            padding:           "0.2em",
            'padding-left':    "1.2em",
            cursor:            "pointer"
        },
        ".rchemo-dark .rchemo-edit-container li:hover": {
            background:        "rgb(36, 43, 49)"
        },
        ".rchemo-edit-container li:hover": {
            background:        "rgb(238, 246, 253)"
        },
        ".rchemo-edit-container li.checked": {
            background:        "rgb(200, 223, 244)"
        },
        ".rchemo-dark .rchemo-edit-container li.checked": {
            background:        "rgb(79, 91, 102)"
        },
        ".rchemo-edit-container li.checked::before": {
            position:          "absolute",
            left:              "0.2em",
            content:           "'\\2713'",
            color:             "rgb(43, 151, 20)"
        },
        ".rchemo-dark .rchemo-edit-container li.checked::before": {
            color:             "rgb(129, 238, 106)"
        },
        ".rchemo-edit-buttons": {
            'text-align':      "right",
            margin:            "0.4em 0 0.2em 0",
            'padding-right':   "0.5em"
        },
        ".rchemo-edit-cancel, .rchemo-edit-submit": {
            color:             "#eee",
            padding:           "0.2em 0.5em",
            "border-radius":   "0.1em",
            'font-size':       "0.9em"
        },
        ".rchemo-edit-cancel": {
            background:        "rgb(135, 135, 135)"
        },
        ".rchemo-edit-cancel:hover": {
            background:        "rgb(90, 90, 90)"
        },
        ".rchemo-edit-submit": {
            background:        "rgb(135, 158, 200)",
            "margin-left":     "0.6em",
        },
        ".rchemo-edit-submit:hover": {
            background:        "rgb(67, 104, 170)"
        }
    };
    let styletxt = "";
    for(let selector in styles) {
        styletxt += `${selector} {`;
        for(let skey in styles[selector]) {
            styletxt += `${skey}: ${styles[selector][skey]};`;
        }
        styletxt += "}";
    }
    let styleElem = document.createElement('style');
    styleElem.className = 'rchemo-styles';
    styleElem.innerText = styletxt;
    document.body.appendChild(styleElem);
    var cssRules = Array.from(document.styleSheets[document.styleSheets.length-1].cssRules);
    //-------------------------------------------------------------------------------------
    // handlers for banned list
    //-------------------------------------------------------------------------------------
    GM_getValue = GM_getValue || GM.getValue;
    GM_setValue = GM_setValue || GM.setValue;
    var banSubreddits = refreshBanned();
    function refreshBanned() {
        banSubreddits = (
            (GM_getValue("banned") && GM_getValue("banned").split("|"))
            || window.bannedSubreddits
            || (unsafeWindow && unsafeWindow.bannedSubreddits)
            || []
        );
        banSubreddits = banSubreddits.map(n => n.trim().toLowerCase())
                                     .map(n => n.startsWith("r/") ? n : `r/${n}`);
        banSubreddits.sort();
        return banSubreddits;
    }
    function addBanned(subreddit) {
        banSubreddits.push(subreddit.trim().toLowerCase().startsWith("r/") ? subreddit : `r/${subreddit}`);
        banSubreddits.sort();
        GM_setValue("banned", banSubreddits.join("|"));
    }
    function removeBanned(subreddit) {
        subreddit = subreddit.startsWith("r/") ? subreddit : `r/${subreddit}`;
        let index = banSubreddits.indexOf(subreddit);
        if(~index) {
            banSubreddits.splice(index, 1);
            GM_setValue("banned", banSubreddits.join("|"));
        }
    }
    //-------------------------------------------------------------------------------------
    // control element and options
    //-------------------------------------------------------------------------------------
    var controlElem = document.createElement('div');
    controlElem.className = 'rchemo-counter';
    controlElem.innerHTML = (
        "<p style='font-weight:bold'>Reddit Chemo</p>" +
        "<div class='rchemo-counter-content'>" +
          "<p>Posts blocked: <span class='rchemo-count'>0</span></p>" +
          "<p>Ads blocked: <span class='rchemo-adcount'>0</span></p>" +
          "<button class='rchemo-btn-showhide'></button>" +
          "<button class='rchemo-btn-editlist'>Edit banned list</button>" +
          "<button class='rchemo-btn-darkmode'>light mode</button>" +
          "<a href=\"https://ko-fi.com/F1F25YGLA\" rel=\"nofollow\" target=\"blank\"><button class='rchemo-btn-support'>Buy me a coffee</button></a>" +
        "</div>"
    );
    document.body.appendChild(controlElem);
    function showControl() { controlElem.style.display = ""; }
    function hideControl() { controlElem.style.display = "none"; }

    var countElem = controlElem.querySelector(".rchemo-count"),
        adCountElem = controlElem.querySelector(".rchemo-adcount"),
        adblockCount = 0,
        blockCount = 0;
    function resetCount() { countElem.innerHTML = blockCount = adblockCount = 0; }
    function incrementCount() { countElem.innerHTML = ++blockCount; }
    function incrementAdCount() { adCountElem.innerHTML = ++adblockCount; }

    var showHideBtn = controlElem.querySelector(".rchemo-btn-showhide"),
        postCssRule = cssRules.filter(r => r.selectorText == ".rchemo-post")[0],
        showBanned  = GM_getValue("showbanned");
    if(showBanned === null || typeof showBanned === "undefined") showBanned = true;
    function setShowBanned(visible) {
        showBanned = !!visible;
        GM_setValue("showbanned", showBanned);
        showHideBtn.innerHTML = (showBanned ? "Hide" : "Show") + " blocked posts";
        postCssRule.style.display = showBanned ? "" : "none";
    };
    setShowBanned(showBanned);
    showHideBtn.addEventListener('click', () => setShowBanned(!showBanned));

    var darkBtn = controlElem.querySelector(".rchemo-btn-darkmode"),
        darkmode = GM_getValue("darkmode");
    function setDarkMode(darkOn) {
        darkmode = !!darkOn;
        GM_setValue("darkmode", darkOn);
        if(darkmode) {
            document.body.classList.add("rchemo-dark");
            darkBtn.innerHTML = "dark mode";
        } else {
            document.body.classList.remove("rchemo-dark");
            darkBtn.innerHTML = "light mode";
        }
    }
    setDarkMode(darkmode);
    darkBtn.addEventListener('click', () => setDarkMode(!darkmode));
    //-------------------------------------------------------------------------------------
    // editing the list
    //-------------------------------------------------------------------------------------
    var editBtn = controlElem.querySelector(".rchemo-btn-editlist");
    editBtn.addEventListener('click', evt => {
        evt.stopPropagation();
        openEditor();
    });
    var editWindow = null;
    function closeEditor(unbanList) {
        if(editWindow) {
            editWindow.remove();
            editWindow = null;
        }
        if(!unbanList || !unbanList.length) return;
        unbanList.forEach(subreddit => removeBanned(subreddit));
    }
    function openEditor() {
        if(editWindow) closeEditor();
        editWindow = document.createElement('div');
        editWindow.className = 'rchemo-edit-container';
        editWindow.innerHTML = (
            "<p style='font-weight:bold'>Reddit Chemo (Ban List)</p>" +
            "<p style='margin:0.4em 0;font-size:0.9em;'>Select/highlight subreddits to remove from the ban list below. (A refresh will be required to show previously hidden posts.)</p>" +
            "<ul class='rchemo-edit-list'></ul>" +
            "<div class='rchemo-edit-buttons'>" +
              "<button class='rchemo-edit-cancel'>Cancel</button>" +
              "<button class='rchemo-edit-submit'>Apply</button>" +
            "</div>"
        );
        var listElem = editWindow.querySelector(".rchemo-edit-list"),
            unban = [];
        banSubreddits.forEach(subreddit => {
            let listSubreddit = document.createElement("li");
            listSubreddit.innerHTML = subreddit;
            listSubreddit.addEventListener('click', () => {
                let ubindex = unban.indexOf(subreddit);
                if(~ubindex) {
                    unban.splice(ubindex, 1);
                    listSubreddit.classList.remove("checked");
                } else {
                    unban.push(subreddit);
                    listSubreddit.classList.add("checked");
                }
            });
            listElem.append(listSubreddit);
        });
        editWindow.querySelector(".rchemo-edit-cancel").addEventListener('click', closeEditor);
        editWindow.querySelector(".rchemo-edit-submit").addEventListener('click', () => closeEditor(unban));
        document.body.appendChild(editWindow);
    }
    document.body.addEventListener('click', evt => {
        if(!editWindow) return;
        if(!editWindow.contains(evt.target)) closeEditor();
    });
    //-------------------------------------------------------------------------------------
    // block post function
    //-------------------------------------------------------------------------------------
    function blockPost(post, sub, mode) {
        post.classList.add("rchemo-post");
        if(mode == 'compact') post = post.children[0];
        let child, voteElem, subelm, icon;
        Array.from(post.children).forEach(child => {
            if(child.getAttribute("data-click-id") === "background") {
                child.innerHTML = `Post from ${sub} removed`;
                let rmvBtn = document.createElement("button");
                rmvBtn.innerHTML = `Remove ban`;
                rmvBtn.classList.add("rchemo-unban");
                rmvBtn.addEventListener('click', function() {
                    this.parentNode.innerHTML = `Post from ${sub} removed from banned list (refresh for reload)`;
                    this.remove();
                    removeBanned(sub);
                });
                child.append(rmvBtn);
                return;
            }
            let downvote = child.querySelectorAll("#vote-arrows-"+post.id + " button[data-click-id='downvote']");
            if(downvote && downvote.length) {
                child.style.top = "-0.7em";
                for(let j = 0; j < downvote.length; ++j) {
                    let voteElem = downvote[j].parentNode;
                    voteElem.style.margin = 0;
                    voteElem.style.padding = 0;
                    voteElem.parentNode.style.border = "none";
                    voteElem.innerHTML = "";
                    voteElem.append(downvote[j]);
                }
                return;
            }
            child.remove()
        });
        incrementCount();
    }
    //-------------------------------------------------------------------------------------
    // watchers as React will sometimes restore/readd posts
    //-------------------------------------------------------------------------------------
    var emptyObserver = new MutationObserver(mutated => {
        mutated.forEach(mutant => {
            if(mutant.target.children.length) {
                mutant.target.innerHTML = "";
                mutant.target.style.border = "none";
                mutant.target.style.fill = "none";
            }
        });
    });
    var blockObserver = new MutationObserver(mutated => {
        mutated.forEach(mutant => {
            if(mutant.target.children.length) {
                let post = mutant.target.closest("div[data-testid='post-container']");
                blockPost(post, post.getAttribute("chemo"));
            }
        });
    });
    function refreshObservers() {
        emptyObserver.disconnect();
        blockObserver.disconnect();
    };
    //-------------------------------------------------------------------------------------
    // resolvers for when post data is not yet loaded
    //-------------------------------------------------------------------------------------
    function watchPost(post) {
        (new MutationObserver((mutated, observer) => {
            if(checkAd(post)) return observer.disconnect();
            let subreddit = getSubredditNode(post);
            if(checkBanned(post, subreddit)) return observer.disconnect();
        })).observe(post, {childList:true, subtree:true});
    }
    function watchSubreddit(post, subreddit) {
        (new MutationObserver((mutated, observer) => {
            if(checkBanned(post, subreddit)) observer.disconnect();
        })).observe(subreddit, {childList:true, attributes:true});
    }
    //-------------------------------------------------------------------------------------
    // process/check post functions
    //-------------------------------------------------------------------------------------
    function checkAd(post) {
        if(
            Array.from(post.querySelectorAll("span"))
                 .find(span => span.innerText && span.innerText.toLowerCase() === "promoted")
        ) {
            post.innerHTML = "";
            post.style.border = "none";
            post.style.fill = "none";
            console.log("Ad removed.");
            emptyObserver.observe(post, {childList:true});
            incrementAdCount();
            return 1;
        }
        return 0;
    }
    function checkBanned(post, subreddit) {
        let mode = 'classic';
        if(post.children.length === 1) {
            mode = 'compact';
            post.classList.add("rchemo-compact");
        } else if(post.querySelectorAll("a[data-click-id='subreddit']").length > 1) {
            mode = 'card';
            post.classList.add("rchemo-card");
        } else {
            post.classList.add("rchemo-classic");
        }
        if(subreddit && subreddit.innerText) {
            let subname = subreddit.innerText.toLowerCase();
            if(~banSubreddits.indexOf(subname)) {
                post.setAttribute("chemo", subreddit.innerText);
                blockPost(post, subname, mode);
                console.log(`Banned subreddit (${subname}) post removed.`);
                blockObserver.observe(
                    post.querySelector("div[data-click-id='background']"),
                    {childList:true}
                );
                return 1;
            }
            let addBtn = document.createElement("button");
            addBtn.innerHTML = `Ban`;
            addBtn.classList.add("rchemo-ban");
            addBtn.addEventListener('click', function() {
                this.remove();
                addBanned(subname);
                blockPost(post, subname, mode);
            });
            let subscribeBtn = post.querySelector("#subscribe-button-"+post.id);
            if(subscribeBtn) {
                subscribeBtn.after(addBtn);
                subscribeBtn.classList.add("rchemo-join");
            } else if(mode == 'compact') {
                subreddit.after(addBtn);
            }
            return -1;
        }
        return 0;
    }
    function getSubredditNode(post) {
        let subreddit = post.querySelectorAll("a[data-click-id='subreddit']");
        if(!subreddit || !subreddit.length) return null;
        for(let i = 0; i < subreddit.length; ++i) {
            // card layout has two subreddit click elements, one with icon/image
            if(!subreddit[i].children.length) return subreddit[i];
        }
    }
    function processNodes(nodes, refresh) {
        if(!nodes || !nodes.length) {
            if(refresh) hideControl();
            return;
        }
        let found = 0;
        nodes.forEach(node => {
            if(!node || !node.querySelectorAll) return;
            node.querySelectorAll("div[data-testid='post-container']").forEach(post => {
                ++found;
                if(post.getAttribute("chemo")) return;
                post.setAttribute("chemo", 1);
                if(checkAd(post)) return;
                let subreddit = getSubredditNode(post);
                if(!subreddit) return watchPost(post);
                if(!checkBanned(post, subreddit)) watchSubreddit(post, subreddit);
            });
        });
        if(refresh) {
            found > 1 ? showControl() : hideControl();
        }
    }
    //-------------------------------------------------------------------------------------
    // init
    //-------------------------------------------------------------------------------------
    processNodes([document.querySelector(".ListingLayout-outerContainer")], true);
    //-------------------------------------------------------------------------------------
    // if Reddit Watcher available, use it for update/change hooks
    //-------------------------------------------------------------------------------------
    let redditWatcher = window.redditWatcher || (unsafeWindow && unsafeWindow.redditWatcher);
    if(redditWatcher) {
        redditWatcher.feed.onChange(feed => {
            resetCount();
            refreshObservers();
            processNodes([feed], true);
        });
        redditWatcher.feed.onUpdate((feed, mutated) => {
            mutated && mutated.forEach(mutant => processNodes(mutant.addedNodes));
        });
        return;
    }
    //-------------------------------------------------------------------------------------
    // otherwise manually create watcher
    //-------------------------------------------------------------------------------------
    function getFeedWrapper() {
        let listingLayout = document.querySelector(".ListingLayout-outerContainer"),
            firstPost     = listingLayout && listingLayout.querySelector("div[data-testid='post-container']"),
            feedWrapper   = firstPost && firstPost.parentNode;
        while(feedWrapper && !feedWrapper.nextSibling) {
            if(feedWrapper == listingLayout) return null;
            feedWrapper = feedWrapper.parentNode || null;
        }
        return feedWrapper && feedWrapper.parentNode;
    }
    var feedWatcher = new MutationObserver(mutated => mutated.forEach(mutant => processNodes(mutant.addedNodes))),
        lastFeedWrapper = null;
    (new MutationObserver(() => {
        let feedWrapper = getFeedWrapper();
        if(feedWrapper !== lastFeedWrapper) {
            resetCount();
            refreshObservers();
            feedWatcher.disconnect();
            if(feedWrapper) {
                processNodes([feedWrapper], true);
                feedWatcher.observe(feedWrapper, {childList:true});
                lastFeedWrapper = feedWrapper;
            }
        }
    })).observe(document.body, {childList:true, subtree:true});
})();