// ==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"> ? </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();
})();