Skip to content

Instantly share code, notes, and snippets.

@myfonj
Created March 25, 2021 11:34
Show Gist options
  • Save myfonj/932c50067ed4d112bdf4366ff6cc2309 to your computer and use it in GitHub Desktop.
Save myfonj/932c50067ed4d112bdf4366ff6cc2309 to your computer and use it in GitHub Desktop.
// ==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
// @author myfonj
// ==/UserScript==
// Styles
document.head.appendChild(document.createElement('style')).textContent = `
.new.new.new {
border-right: 2px solid #0F06; display: block; padding-right: 1em;
}
.justSeen.justSeen.justSeen {
border-right: 2px solid #0F03; display: block; padding-right: 1em;
}
.seen.seen.seen.seen {
/* leave it as it was */
}
`;
const VISIBILITY_DURATION_MS = 900;
const CLASSES = { new: 'new', seen: 'seen', justSeen: 'justSeen' };
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 = (localStorage.getItem(LS_KEY) || '').split(',');
const ELS_TO_WATCH = document.querySelectorAll('.commtext,.storylink');
const VIEWPORT_OBSERVER = new IntersectionObserver(
(entries, observer) => {
entries.forEach(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.justSeen);
SEEN_HASH_LIST.push(MAP_EL_HASH.get(TGT))
MAP_EL_TIMEOUT.delete(TGT);
VIEWPORT_OBSERVER.unobserve(TGT);
// TODO move the saving to LS to unload
// and blur for less writes
localStorage.setItem(
LS_KEY,
SEEN_HASH_LIST.join(',')
);
}
}, VISIBILITY_DURATION_MS)
);
} else {
MAP_EL_TIMEOUT.delete(TGT);
}
});
},
{
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.includes(hash)) {
el.classList.add(CLASSES.seen);
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)
)
)
)
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment