Skip to content

Instantly share code, notes, and snippets.

@brianloveswords
Last active July 16, 2018 00:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brianloveswords/d7c5d390c6a807c36691e16e9adbc724 to your computer and use it in GitHub Desktop.
Save brianloveswords/d7c5d390c6a807c36691e16e9adbc724 to your computer and use it in GitHub Desktop.

Identifying Non-Chronological Tweets In Timeline

[jump to complete source]

There are two stages to this:

  1. identify initially loaded non-chrono tweets
  2. 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);

Totally Unrelated Links

const getInitialTweets = () => [...document.querySelectorAll('.js-stream-item')]
const nonchrono = e => {
for (typ of ['follower', 'promoted', 'heartBadge']) {
if (e.querySelector(`.context .Icon--${typ}`)) {
return true;
}
}
return false;
}
const decorate = e => e.style.border = "10px solid red";
const remove = e => e.remove();
const handler = decorate;
const processTweets = tweets => tweets.filter(nonchrono).forEach(handler);
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)
}
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);
}
const tweetObserver = createTweetObserver();
processTweets(getInitialTweets());
attachTweetObserver(tweetObserver);
attachDocObserver(tweetObserver);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment