HN: style unread content

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

当前为 2021-04-13 提交的版本,查看 最新版本

// ==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.3
// @author      myfonj
// ==/UserScript==

// https://gf.qytechs.cn/en/scripts/423969/versions/new

// kinda sorta configuration
const SELECTOR = '.commtext, .storylink';
const VIEWPORT_EXPOSITION_DURATION_UNTIL_READ = 900;
const CSS_CLASSES = { unread: 'new', read: 'read', old: 'old' };
// Styling. Very lame for now.
const CSS_STR = `
.${CSS_CLASSES.unread}  { border-right: 2px solid #3F36; display: block; padding-right: 1em; }
.${CSS_CLASSES.read} { border-right: 2px solid #0F03; display: block; padding-right: 1em; }
.${CSS_CLASSES.old}  { /* nothing */ }
`;
// base64 'SHA-1' digest hash = 28 characters; most probably ending with '=' always (?)
// could be used for splitting pergaps, but not used for now.
const HASH_DIGEST_ALGO = 'SHA-1';

// actual code, yo
document.head.appendChild(document.createElement('style')).textContent = CSS_STR;
// TODO mutation observer for client-side rendered pages (not HNs case)
const ELS_TO_WATCH = document.querySelectorAll(SELECTOR);
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 VIEWPORT_ENTRY_CHECKER = (entry) => {
  const TGT = entry.target;
  if (entry.isIntersecting) {
    // entered viewport
    if (MAP_EL_TIMEOUT.get(TGT)) {
      // already measuring - quick re-entry
      return
    }
    // measure time in viewport
    MAP_EL_TIMEOUT.set(
      TGT,
      window.setTimeout(function () {
        if (MAP_EL_TIMEOUT.get(TGT)) {
          // HA! STILL in viewport!
          // mark as read
          TGT.classList.remove(CSS_CLASSES.unread);
          TGT.classList.add(CSS_CLASSES.read);
          SEEN_HASH_LIST.add(MAP_EL_HASH.get(TGT))
          MAP_EL_TIMEOUT.delete(TGT);
          // not interested in this element anymore
          VIEWPORT_OBSERVER.unobserve(TGT);
          // TODO move the persistence to unload and/or blur for fewer LS writes
          localStorage.setItem(
            LS_KEY,
            Array.from(SEEN_HASH_LIST).join(',')
          );
        }
      }, VIEWPORT_EXPOSITION_DURATION_UNTIL_READ)
    );
  } else {
    // left viewport
    MAP_EL_TIMEOUT.delete(TGT);
  }
}
const VIEWPORT_OBSERVER = new IntersectionObserver(
  (entries, observer) => {
    entries.forEach(_ => VIEWPORT_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(CSS_CLASSES.old);
    return;
  }
  el.classList.add(CSS_CLASSES.unread);
  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)
        )
      )
    )
  );
};

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址