HN: style unread content

Save hashes of displayed comments locally and mark new ones when displayed for the first time

От 13.04.2021. Виж последната версия.

// ==UserScript==
// @name        HN: style unread content 
// @description Save hashes of displayed comments locally and mark new ones when displayed for the first time
// @namespace   myfonj
// @match       https://news.ycombinator.com/*
// @grant       none
// @version     1.0.2
// @author      myfonj
// ==/UserScript==

// https://greatest.deepsurf.us/en/scripts/423969/versions/new

// Styles. Just lame green-transparent right border, for now.

const VISIBILITY_DURATION_MS = 900;
const CLASSES = { new: 'new', read: 'read', old: 'old' };
document.head.appendChild(document.createElement('style')).textContent = `
.${CLASSES.new}  { border-right: 2px solid #3F36; display: block; padding-right: 1em; }
.${CLASSES.read} {	border-right: 2px solid #0F03; display: block; padding-right: 1em; }
.${CLASSES.old}  { /* nothing */ }
`;
const HASH_DIGEST_ALGO = 'SHA-1'; // 'SHA-256';
const MAP_EL_HASH = new WeakMap();
const MAP_EL_TIMEOUT = new WeakMap();
const LS_KEY = 'displayed_hashes_' + HASH_DIGEST_ALGO;
const SEEN_HASH_LIST = new Set((localStorage.getItem(LS_KEY) || '').split(','));
const ELS_TO_WATCH = document.querySelectorAll('.commtext,.storylink');
const ENTRY_CHECKER = (entry) => {
  const TGT = entry.target;
  if (entry.isIntersecting) {
    if (MAP_EL_TIMEOUT.get(TGT)) {
      // don't set twice on re-entry
      return
    }
    MAP_EL_TIMEOUT.set(
      TGT,
      window.setTimeout(function () {
        if (MAP_EL_TIMEOUT.get(TGT)) {
          TGT.classList.remove(CLASSES.new);
          TGT.classList.add(CLASSES.read);
          SEEN_HASH_LIST.add(MAP_EL_HASH.get(TGT))
          MAP_EL_TIMEOUT.delete(TGT);
          VIEWPORT_OBSERVER.unobserve(TGT);
          // TODO move the saving to LS to unload and/or blur for fewer writes
          localStorage.setItem(
            LS_KEY,
            Array.from(SEEN_HASH_LIST).join(',')
          );
        }
      }, VISIBILITY_DURATION_MS)
    );
  } else {
    MAP_EL_TIMEOUT.delete(TGT);
  }
}
const VIEWPORT_OBSERVER = new IntersectionObserver(
	(entries, observer) => {
		entries.forEach(_=>ENTRY_CHECKER(_));
	},
	{
		root: null,
		rootMargin: "-9%", // TODO use "lines" height here?
		threshold: 0
	}
);
ELS_TO_WATCH.forEach(async el => {
	const hash = await makeHash(el.textContent);
	if (SEEN_HASH_LIST.has(hash)) {
		el.classList.add(CLASSES.old);
		return;
	}
	el.classList.add(CLASSES.new);
	MAP_EL_HASH.set(el, hash);
	VIEWPORT_OBSERVER.observe(el);
});
async function makeHash (input) {
	return btoa(
		String.fromCharCode.apply(
			null,
			new Uint8Array(
				await crypto.subtle.digest(
					HASH_DIGEST_ALGO,
					(new TextEncoder()).encode(input)
				)
			)
		)
	);
};