Twitter Filter

Auto hide tweets according to your rules (shortcut key H toggles hiding)

// ==UserScript==
// @name         Twitter Filter
// @namespace    zeusex81@gmail.com
// @version      4.0
// @description  Auto hide tweets according to your rules (shortcut key H toggles hiding)
// @include      https://twitter.com/*
// @include      https://mobile.twitter.com/*
// @icon         https://i.imgur.com/6960LbS.png
// @resource     logo https://i.imgur.com/fywtc68.png
// @license      MIT
// @grant        GM.getResourceUrl
// @grant        GM.getValue
// @grant        GM.setValue
// @noframes
// ==/UserScript==

(async function() {
    let settings, newers, keywords, observer, refresh = true;
  	const defaults = '{"mode":1, "retweet":true, "blocking":true, "muting":true, "image":false, "tweets":false, "tweetsValue":100, "subscriptions":false, '+
                     '"subscriptionsValue":50, "followers":false, "followersValue":10, "newers":false, "newersValue":5, "keywords":false, "keywordsValue":""}';
    const styles = [document.createElement('style'), document.createElement('style'), document.createElement('style')];
    styles[0].innerHTML = '.twitterFilter { max-height: 1000px; transition: max-height 0.5s; align-items:start; overflow:hidden; }';
    styles[1].innerHTML = '.twitterFilter { max-height:   20px; transition: max-height 0.5s; align-items:start; overflow:hidden; }\n article:hover .twitterFilter { max-height: 1000px; }';
    styles[2].innerHTML = '.twitterFilter { max-height:    0px; transition: max-height 0.5s; align-items:start; overflow:hidden; }';
    const updateSettings = function() {
        newers   = Date.now() - settings.newersValue * 365.2425 / 12 * 24 * 60 * 60 * 1000;
        keywords = new RegExp(settings.keywordsValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '|'), 'i');
        for(let i=0; i<3; ++i)
            if(i == Math.min(settings.mode, 2)) {
                if(!styles[i].parentNode) document.head.appendChild(styles[i]);
            } else {
                if( styles[i].parentNode) styles[i].remove();
            }
    };
    const loadSettings = async function() { settings = JSON.parse(await GM.getValue('twitterFilter', defaults)); await updateSettings(); };
    const saveSettings = async function() { await GM.setValue('twitterFilter', JSON.stringify(settings))       ; await updateSettings(); };
    await loadSettings();

    const ids = [], users = [], regex = /\/(mutes|blocks)\/.*create|\/timeline\/(?!home)|\/search\//i;
    const twitterFilter = function() {
        if(this.readyState == 4 && this.status == 200 && regex.test(this.responseURL))
        try {
            const data = JSON.parse(this.responseText);
            if(data.globalObjects) {
                for(const user of Object.values(data.globalObjects.users))
                    if(!ids.includes(user.id_str) && (
                        (settings.blocking && user.blocking) || (settings.muting && user.muting) || (settings.image && user.default_profile_image) ||
                        (settings.tweets && user.statuses_count < settings.tweetsValue) || (settings.subscriptions && user.friends_count < settings.subscriptionsValue) ||
                        (settings.followers && user.followers_count < settings.followersValue) || (settings.newers && Date.parse(user.created_at) > newers) ||
                        (settings.keywords && (keywords.test(user.name) || keywords.test(user.screen_name) || keywords.test(user.description))) ))
                    {
                        ids.push(user.id_str);
                        users.push(user.screen_name);
                    }
                if(settings.mode == 3) {
                    let changed = false;
                    for(const tweet of Object.values(data.globalObjects.tweets))
                        if((settings.retweet && tweet.retweeted_status_id_str) || ids.includes(tweet.user_id_str)) {
                            delete data.globalObjects.tweets[tweet.id_str];
                            changed = true;
                        }
                    if(changed) {
                        Object.defineProperty(this, "responseText", {writable: true});
                        this.responseText = JSON.stringify(data);
                    }
                }
            } else if(!ids.includes(data.id_str)) {
                ids.push(data.id_str);
                users.push(data.screen_name);
                refresh = true;
            }
        } catch(e) {}
        if(this.twitterFilterOnreadystatechange) this.twitterFilterOnreadystatechange.apply(this, arguments);
    };
    const oldSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function() {
        this.twitterFilterOnreadystatechange = this.onreadystatechange;
        this.onreadystatechange = twitterFilter;
        oldSend.apply(this, arguments);
    };

    const menu = document.createElement('div');
    menu.innerHTML =
        '<img style="border-radius:50%; margin-left:8px;" onmouseover="this.style.background=\'rgba(128,128,128,0.2)\';" onmouseout="this.style.background=\'\'">'+
        '<form style="position:absolute; top:8px; right:8px; display:none; padding:8px; border-radius:8px; background-color:rgba(0,0,0,0.8); color:white;">'+
            '<fieldset style="margin-bottom:8px; border-radius:8px;">'+
                '<legend>Mode</legend>'+
                '<label><input type="radio" name="twitterFilter">Show</label>'+
                '<label><input type="radio" name="twitterFilter">Mask</label>'+
                '<label><input type="radio" name="twitterFilter">Hide</label>'+
                '<hr style="color:white;">'+
                '<label><input type="radio" name="twitterFilter">Remove</label>'+
            '</fieldset>'+
            '<label><input type="checkbox">when Retweet</label><br>'+
            '<label><input type="checkbox">when Blocking user</label><br>'+
            '<label><input type="checkbox">when Muting user</label><br>'+
            '<label><input type="checkbox">when Default Profile Image</label><br>'+
            '<label><input type="checkbox">when less than <input type="number" min="1" style="border:0; width:40px; background-color:transparent; color:white;"> Tweets</label><br>'+
            '<label><input type="checkbox">when less than <input type="number" min="1" style="border:0; width:40px; background-color:transparent; color:white;"> Subscriptions</label><br>'+
            '<label><input type="checkbox">when less than <input type="number" min="1" style="border:0; width:40px; background-color:transparent; color:white;"> Followers</label><br>'+
            '<label><input type="checkbox">when less than <input type="number" min="1" style="border:0; width:40px; background-color:transparent; color:white;"> Months</label> '+
            '<span style="border:1px solid; border-radius:50%;" title="i.e. how old the user account is, not the tweet">&nbsp;?&nbsp;</span><br>'+
            '<label><input type="checkbox">when name/description includes :<br>'+
            '<textarea style="width:90%; margin:0 5% 8px; resize:vertical;" placeholder="case insensitive\nseparate words with space"></textarea><br>'+
            '<input type="button" value="Save" style="border-radius:16px; width:49%; margin-right:1%;">'+
            '<input type="button" value="Cancel" style="border-radius:16px; width:49%; margin-left:1%;">'+
        '</form>';
    const inputs = menu.querySelectorAll('input,textarea');
    menu.onfocus = function() { menu.lastChild.style.display = ''; };
    menu.onblur  = function() { menu.lastChild.style.display = 'none'; };
    menu.firstChild.src = await GM.getResourceUrl('logo');
    menu.firstChild.onclick = async function() {
        await loadSettings();
        inputs[settings.mode].checked = true;
        inputs[ 4].checked = settings.retweet;
        inputs[ 5].checked = settings.blocking;
        inputs[ 6].checked = settings.muting;
        inputs[ 7].checked = settings.image;
        inputs[ 8].checked = settings.tweets;
        inputs[ 9].value   = settings.tweetsValue;
        inputs[10].checked = settings.subscriptions;
        inputs[11].value   = settings.subscriptionsValue;
        inputs[12].checked = settings.followers;
        inputs[13].value   = settings.followersValue;
        inputs[14].checked = settings.newers;
        inputs[15].value   = settings.newersValue;
        inputs[16].checked = settings.keywords;
        inputs[17].value   = settings.keywordsValue;
        menu.onfocus();
    };
    inputs[18].onclick = async function() {
        settings.mode               = inputs[ 0].checked ? 0 : inputs[1].checked ? 1 : inputs[2].checked ? 2 : 3;
        settings.retweet            = inputs[ 4].checked;
        settings.blocking           = inputs[ 5].checked;
        settings.muting             = inputs[ 6].checked;
        settings.image              = inputs[ 7].checked;
        settings.tweets             = inputs[ 8].checked;
        settings.tweetsValue        = inputs[ 9].value;
        settings.subscriptions      = inputs[10].checked;
        settings.subscriptionsValue = inputs[11].value;
        settings.followers          = inputs[12].checked;
        settings.followersValue     = inputs[13].value;
        settings.newers             = inputs[14].checked;
        settings.newersValue        = inputs[15].value;
        settings.keywordsValue      = inputs[17].value.trim();
        settings.keywords           = inputs[16].checked && settings.keywordsValue.length>0;
        menu.onblur();
        await saveSettings();
    };
    inputs[19].onclick = menu.onblur;

    addEventListener('keyup', async function(e) {
        if(e.keyCode == 72 && document.activeElement.tagName != 'INPUT' && document.activeElement.tagName != 'TEXTAREA' && !document.activeElement.hasAttribute('contenteditable')) {
            await loadSettings();
            if(settings.mode < 3) {
                settings.mode = (settings.mode+1) % 3;
                await saveSettings();
            }
            updateSettings();
        }
    });
    const update = function() {
        if(location.pathname != '/home' && document.body.style.overflowY != 'hidden') {
            if(menu.parentNode) {
                menu.remove();
                menu.onblur();
            }
            if(!observer) {
                const e = document.querySelector('main');
                if(e) (observer = new MutationObserver(function() { refresh = true; })).observe(e, {childList: true, subtree: true});
            } else if(refresh) {
                for(const article of document.getElementsByTagName('article')) {
                    const tweet = article.querySelector('article > div > div:last-child[data-testid="tweet"]');
                    if(tweet && (users.includes(article.querySelector('a').pathname.substr(1)) ||
                                 (settings.retweet && article.querySelector('article > div > div:first-child svg')) ))
                        settings.mode < 3 ? tweet.classList.add('twitterFilter') : article.remove();
                }
                refresh = false;
            }
        } else if(!menu.parentNode) {
            const e = document.querySelector('path[d^="M22.772"]');
            if(e) e.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.appendChild(menu);
        }
        requestAnimationFrame(update);
    };
    update();
})();