Skip to content

Instantly share code, notes, and snippets.

@justjanne
Last active September 21, 2021 17:05
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 justjanne/e61fcc9edb7d3dc60bc1a788d0329a0e to your computer and use it in GitHub Desktop.
Save justjanne/e61fcc9edb7d3dc60bc1a788d0329a0e to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Highlight Comments: Hacker News
// @namespace de.kuschku.highlight-comments.hn
// @version 1.0.4
// @include https://news.ycombinator.com/item*
// @include https://news.ycombinator.com/reply*
// ==/UserScript==
function contentScript() {
const LOCAL_STORAGE_KEY = "highlight-comments";
const VISIT_INTERVAL = 10 * 60 * 1000;
const translation = {
"label.select-visit": "Highlight comments since last visit: ",
"label.no-highlighting": "No Highlighting"
};
function parseDate(value) {
try {
const result = new Date(value);
if (isNaN(result.getTime())) {
return null;
}
return result;
} catch (_) {
return null;
}
}
class LocalVisitStorage {
data = {};
constructor() {
this.data = LocalVisitStorage.__read();
}
get(threadId) {
this.data = LocalVisitStorage.__read();
const visits = this.data[threadId] || [];
visits.sort();
return visits.map(parseDate);
}
add(threadId, timestamp) {
this.data = LocalVisitStorage.__read();
if (!this.data[threadId]) {
this.data[threadId] = [];
}
this.data[threadId].push(timestamp.toISOString());
LocalVisitStorage.__write(this.data);
}
clear(threadId) {
delete this.data[threadId];
LocalVisitStorage.__write(this.data);
}
static __write(data) {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(data));
}
static __read() {
const data = localStorage.getItem(LOCAL_STORAGE_KEY);
if (!data) {
return {};
}
try {
const parsed = JSON.parse(data);
if (!parsed) {
return {};
}
return parsed;
} catch (_) {
return {};
}
}
}
class HackerNews {
uuid() {
return "abdecd59-6415-4031-8ddc-7a70e740e384";
}
styleInject() {
return `
.new .ind {
border-right: 2px solid #ff6600;
}
.new-comments {
padding: 8px 28px;
font-size: 8pt;
}
`;
}
storyId() {
function getStoryParams() {
const storyOn = document.querySelector(".storyon:not(:empty) a[href]");
if (storyOn) {
return new URLSearchParams(storyOn.search);
}
const fatItem = document.querySelector(".fatitem .age a[href]");
if (fatItem) {
return new URLSearchParams(fatItem.search);
}
return null;
}
const params = getStoryParams();
if (!params) {
return null;
}
return params.get("id");
}
renderUi(visits, onChange) {
const highlightUi = document.createElement("div");
highlightUi.classList.add("new-comments");
const label = document.createElement("label");
const labelText = document.createTextNode(translation["label.select-visit"]);
label.appendChild(labelText);
const select = document.createElement("select");
const nullOption = document.createElement("option");
nullOption.value = "";
nullOption.innerText = translation["label.no-highlighting"];
select.appendChild(nullOption);
for (let timestamp of visits) {
const option = document.createElement("option");
option.value = timestamp.toISOString();
option.innerText = timestamp.toLocaleString();
select.appendChild(option);
}
select.addEventListener("change", () =>
onChange(parseDate(select.value)));
if (visits.length) {
select.value = visits[visits.length - 1].toISOString();
}
onChange(parseDate(select.value));
label.appendChild(select);
highlightUi.appendChild(label);
return highlightUi;
}
injectUi(element) {
const successor = document.querySelector(".comment-tree");
successor.parentElement.insertBefore(element, successor);
}
highlightSinceVisit(visit) {
function getCommentTimestamp(comment) {
const time = comment.querySelector(".comhead .age");
return parseDate(time.title+"Z");
}
function highlightComment(comment, timestamp) {
const commentTimestamp = getCommentTimestamp(comment);
const isNew = timestamp !== null && commentTimestamp !== null &&
commentTimestamp.getTime() > timestamp.getTime();
comment.classList.toggle("new", isNew);
}
const comments = document.querySelectorAll(".athing.comtr");
for (let i = 0; i < comments.length; i++) {
const comment = comments[i];
highlightComment(comment, visit);
}
}
}
class HighlightComments {
constructor(website, storage) {
this.website = website;
this.storage = storage;
this.listener = this.listener.bind(this);
}
listener(visit) {
this.website.highlightSinceVisit(visit);
}
updateUi() {
const storyId = this.website.storyId();
const visits = this.storage.get(storyId);
const oldUi = document.getElementById(this.website.uuid() + "-ui");
if (oldUi) {
oldUi.remove();
}
const userInterface = this.website.renderUi(visits, this.listener);
userInterface.id = this.website.uuid() + "-ui";
this.website.injectUi(userInterface);
const oldStyle = document.getElementById(this.website.uuid() + "-style");
if (oldStyle) {
oldStyle.remove();
}
const style = document.createElement("style");
style.innerText = this.website.styleInject();
style.id = this.website.uuid() + "-style";
document.head.appendChild(style);
}
tryAddVisit() {
const now = new Date();
const storyId = this.website.storyId();
const visits = this.storage.get(storyId);
const lastVisit = visits.length && visits[visits.length - 1];
if (!lastVisit || now.getTime() - lastVisit.getTime() > VISIT_INTERVAL) {
this.storage.add(storyId, now);
}
}
static apply(website, storage) {
const handler = new HighlightComments(website, storage);
handler.updateUi();
handler.tryAddVisit();
}
}
HighlightComments.apply(new HackerNews(), new LocalVisitStorage());
}
function inject() {
const script = document.createElement("script");
script.type = "text/javascript";
script.innerText = "(" + contentScript.toString() + ")();";
document.head.append(script);
}
inject();
@justjanne
Copy link
Author

justjanne commented Sep 21, 2021

Update v1.0.1: Now works with non-UTC local timezones. Only add the new visit after displaying the UI, so the current visit isn’t automatically selected.

@justjanne
Copy link
Author

Update v1.0.2: Now correctly updates the highlighted comments list from the start, instead of requiring to toggle the select twice.

@justjanne
Copy link
Author

Update v1.0.3: Fix the issue mentioned in v1.0.2 again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment