There are two stages to this:
- identify initially loaded non-chrono tweets
- identify newly loaded non-chrono tweets as user scrolls
We start by getting an array of all tweets. querySelectorAll
returns a NodeList
, which unfortunately doesn’t have a filter
method, so we use [...<iterable>]
to collect the results into an array.
const getInitialTweets = () => [...document.querySelectorAll('.js-stream-item')]
We want a filter that keeps only the tweets that don’t belong in the timeline so we can do something with those. One way to identify these tweets is by the icons twitter includes in non-chronological tweets to give the user context of why those tweets are showing up in the timeline.
The nonchrono
function looks at the children of the tweet element and if any of the children are the contextual icon for one of the non-chronological tweets we’re looking for – promoted, other user likes, or “your friends follow this person” – we return true
to keep that tweet.
const nonchrono = e => {
for (typ of ['follower', 'promoted', 'heartBadge']) {
if (e.querySelector(`.context .Icon--${typ}`)) {
return true;
}
}
return false;
}
We also want a function that will do something with the non-chronological tweets we’ve found. In our case we just want to highlight them with a red border. If we wanted to remove those nodes instead, we could use ChildNode.remove.
const decorate = e => e.style.border = "10px solid red";
const remove = e => e.remove();
const handler = decorate;
Putting it together, processTweets
takes a set of tweets, filters down to just the
non-chronological tweets, then decorates them.
const processTweets = tweets => tweets.filter(nonchrono).forEach(handler);
New tweets are loaded once the user scrolls near the bottom of the page, so we need to do something to make sure those new tweets get processed. This is a great opportunity to use MutationObservers.
We set up the MutationObserver
to watch the stream container, .js-navigable-stream
, and we configure it to only care about the list of children. Once we get a mutation event, we can run processTweets
on the list of added nodes.
const createTweetObserver = () => {
return new MutationObserver(mut => processTweets([...mut[0].addedNodes]))
}
const attachTweetObserver = observer => {
const config = { attributes: false, childList: true, subtree: false };
const target = document.querySelector('ol.js-navigable-stream')
observer.observe(target, config)
}
We need one more observer: since twitter dot com is a single page app when the user navigates to their notifications it will unload their timeline, and the node the observer is observing will be destroyed (so we might as well disconnect the observer). Once the timeline is reloaded, we want to re-process the initially loaded tweets and reconnect the observer to the new timeline node.
By observing for attribute changes on the #doc
element we can watch for when the class contains “route-home”, which is an indicator that the main timeline view is active.
const attachDocObserver = (tweetObserver) => {
const config = { attributes: true, childList: false, subtree: false };
const handler = mut => {
tweetObserver.disconnect();
if (mut[0].target.classList.contains('route-home')) {
processTweets(getInitialTweets());
attachTweetObserver(tweetObserver);
}
}
const observer = new MutationObserver(handler);
observer.observe(document.getElementById('doc'), config);
}
With all the functions we need created, we can kick everything off:
const tweetObserver = createTweetObserver();
processTweets(getInitialTweets());
attachTweetObserver(tweetObserver);
attachDocObserver(tweetObserver);
- Greasemonkey (Firefox)
- Tampermonkey (Chrome)