Detecting What's Been Seen

November 23rd, 2023
shrubgrazer, tech
Sometimes it makes sense for sites want to treat things differently based on whether the user has seen them. For example, I like sites that highlight new comments (ex: EA Forum and LessWrong) and I'd like them even better if comments didn't lose their "highlighted" status in cases where I hadn't scrolled them into view. In writing my Mastodon client, Shrubgrazer, I wanted a version of this so it would show me posts that I hadn't seen before. The implementation is a bit fussy, so I wanted to write up a bit on what approach I took.

The code is on github, and it counts posts as viewed if both the top of the post and bottom have been on screen for at least half a second. Specifically, whenever the top or bottom of a post enters the viewport it sets a 500ms timer, and if when the timer fires it's still within the viewport it keeps a record client side. If this now means that both the top and bottom have met the criteria it sends a beacon back so the server can track the entry as viewed.

Go back 4-7 years and this would have required a scroll listener, using a ton of CPU, but modern browsers now support the IntersectionObserver API. This lets us get callbacks whenever an entry enters or leaves the viewport.

I start by creating an IntersectionObserver:

const observer =
  new IntersectionObserver(
    handle_intersect, {
      root: null,
      threshold: [0, 1],
    });

We haven't told it which elements to observe yet, but once we do it will call the handle_intersect callback anytime those elements fully enter or fully exit the viewport.

Each entry has an element at the very top and very bottom, and to start tracking the entry we tell our observer about them:

observer.observe(entry_top);
observer.observe(entry_bottom);

What does our handle_intersect callback do? We maintain two sets of element IDs, onscreen_top and onscreen_bottom for keeping track of what is currently onscreen. The callback keeps those sets current, and also starts the 500ms timer:

function handle_intersect(entries, observer) {
  for (let entry of entries) {
    const target = entry.target;
    const id = target.getAttribute("id");
    const is_bottom =
        target.classList.contains("bottom");
    const onscreen_set =
        is_bottom ? onscreen_bottom : onscreen_top;

    if (entry.intersectionRatio > 0.99) {
      onscreen_set.add(id);
      window.setTimeout(function() {
        onscreen_timeout(
          target, post_id, is_bottom, onscreen_set);
      }, 500);
      start_observation_timer(target);
    } else if (entry.intersectionRatio < 0.01) {
      onscreen_set.delete(id);
    }
  }
}

What does onscreen_timeout do? It checks whether the element is still onscreen, and if it's not then it does nothing. This covers things like fling scrolling where something has been onscreen for such a short time that it really hasn't been seen. Otherwise, if the element is still onscreen, it marks the element as viewed and stops tracking it. And if now both the top and bottom of the entry have been viewed it tells the server about it:

function onscreen_timeout(
    target, post_id, is_bottom, onscreen_set) {
  if (!onscreen_set.has(post_id)) {
    // Element left the screen too quickly,
    // don't track it as being onscreen.
    return;
  }

  observer.unobserve(target);

  if (is_bottom) {
    viewed_bottom.add(post_id);
  } else {
    viewed_top.add(post_id);
  }

  if (viewed_top.has(post_id) &&
      viewed_bottom.has(post_id)) {
    send_view_ping(post_id);
    viewed_top.delete(post_id);
    viewed_bottom.delete(post_id);
  }
}

While Shrubgrazer hasn't had wide usage (I suspect I'm the only user, since it takes some work to host and I'm not hosting for anyone else) this has worked well for me. It makes the browser do almost all the work, so it's very fast.

Comment via: facebook, lesswrong

Recent posts on blogs I like:

Contra Scott Alexander On Apologies

I really need a short word for "complicatedly in favor of"

via Thing of Things September 12, 2024

Don't Help Kids With Contra Dancing If They Don't Need Help

If you're a kid like me, most kids have probably never heard of contra dancing before. You're probably wondering: contra dance -- what's that? Contra dancing is in some ways similar to square dancing. It's a group dance with a caller and…

via Lily Wise's Blog Posts September 9, 2024

Two 19th-century missionary memoirs in China

Life for an American family in 1860s China The post Two 19th-century missionary memoirs in China appeared first on Otherwise.

via Otherwise August 24, 2024

more     (via openring)