|
// ==UserScript== |
|
// @name HN User Info (Age & Karma) |
|
// @namespace https://news.ycombinator.com/ |
|
// @version 1.0 |
|
// @description Shows account age (in days) and karma next to every username on Hacker News |
|
// @match https://news.ycombinator.com/* |
|
// @grant GM_xmlhttpRequest |
|
// @grant GM.xmlHttpRequest |
|
// @connect hacker-news.firebaseio.com |
|
// ==/UserScript== |
|
|
|
(function () { |
|
'use strict'; |
|
|
|
// ── CONFIG ───────────────────────────────────────────────────────── |
|
// Toggle which fields to show |
|
const SHOW_AGE = true; |
|
const SHOW_KARMA = true; |
|
|
|
// Suffix strings (e.g. "d" → "5,055d", " days" → "5,055 days") |
|
const AGE_SUFFIX = 'd'; |
|
const KARMA_SUFFIX = 'k'; |
|
// ────────────────────────────────────────────────────────────────── |
|
|
|
// Cache fetched users so we don't re-fetch duplicates on the same page |
|
const userCache = {}; |
|
|
|
// Calculate age in days from a Unix timestamp |
|
function ageDays(createdUnix) { |
|
const now = Date.now() / 1000; |
|
return Math.floor((now - createdUnix) / 86400); |
|
} |
|
|
|
// Create the badge span that will be inserted after the username |
|
function makeBadge(username) { |
|
const span = document.createElement('span'); |
|
span.className = 'hn-userinfo-badge'; |
|
span.style.cssText = 'color:#828282; font-size:inherit; opacity:0.5;'; |
|
span.textContent = ' …'; |
|
span.dataset.user = username; |
|
return span; |
|
} |
|
|
|
// Format a number with commas (e.g. 13394 → "13,394") |
|
function fmt(n) { |
|
return n.toLocaleString('en-US'); |
|
} |
|
|
|
// Update every badge for a given username with the fetched data |
|
function fillBadges(username, data) { |
|
const days = ageDays(data.created); |
|
const karma = data.karma; |
|
const createdDate = new Date(data.created * 1000); |
|
const dateStr = createdDate.toLocaleDateString('en-US', { |
|
year: 'numeric', month: 'long', day: 'numeric' |
|
}); |
|
|
|
document.querySelectorAll(`.hn-userinfo-badge[data-user="${CSS.escape(username)}"]`).forEach(badge => { |
|
badge.textContent = ''; |
|
badge.style.opacity = '1'; |
|
badge.style.transition = 'opacity 0.3s'; |
|
|
|
if (SHOW_AGE) { |
|
const ageLabel = `${fmt(days)}${AGE_SUFFIX}`; |
|
const ageSpan = document.createElement('span'); |
|
ageSpan.textContent = ` | ${ageLabel}`; |
|
ageSpan.title = dateStr; |
|
ageSpan.style.cssText = 'cursor:help; border-bottom:1px dotted #828282;'; |
|
badge.appendChild(ageSpan); |
|
} |
|
|
|
if (SHOW_KARMA) { |
|
const karmaLabel = `${fmt(karma)}${KARMA_SUFFIX}`; |
|
badge.appendChild(document.createTextNode(` | ${karmaLabel}`)); |
|
} |
|
|
|
if (SHOW_AGE || SHOW_KARMA) { |
|
badge.appendChild(document.createTextNode(' |')); |
|
} |
|
}); |
|
} |
|
|
|
// Fetch user info from the public HN Firebase API |
|
function fetchUser(username) { |
|
if (userCache[username]) return; // already fetched or in-flight |
|
userCache[username] = true; // mark in-flight |
|
|
|
const url = `https://hacker-news.firebaseio.com/v0/user/${encodeURIComponent(username)}.json`; |
|
|
|
// Use GM_xmlhttpRequest if available (Greasemonkey / Tampermonkey), |
|
// otherwise fall back to plain fetch (Violentmonkey, etc.) |
|
const gmXHR = typeof GM_xmlhttpRequest === 'function' |
|
? GM_xmlhttpRequest |
|
: (typeof GM !== 'undefined' && GM.xmlHttpRequest) |
|
? GM.xmlHttpRequest |
|
: null; |
|
|
|
if (gmXHR) { |
|
gmXHR({ |
|
method: 'GET', |
|
url: url, |
|
onload: function (resp) { |
|
try { |
|
const data = JSON.parse(resp.responseText); |
|
if (data && data.created != null) { |
|
userCache[username] = data; |
|
fillBadges(username, data); |
|
} |
|
} catch (_) { /* silently ignore parse errors */ } |
|
} |
|
}); |
|
} else { |
|
fetch(url) |
|
.then(r => r.json()) |
|
.then(data => { |
|
if (data && data.created != null) { |
|
userCache[username] = data; |
|
fillBadges(username, data); |
|
} |
|
}) |
|
.catch(() => {}); |
|
} |
|
} |
|
|
|
// Main: find all .hnuser links, inject badges, kick off fetches |
|
function run() { |
|
const userLinks = document.querySelectorAll('a.hnuser'); |
|
const seen = new Set(); |
|
|
|
userLinks.forEach(link => { |
|
// Skip if we already processed this exact element |
|
if (link.dataset.hnInfoDone) return; |
|
link.dataset.hnInfoDone = '1'; |
|
|
|
// Extract username from href like "user?id=fulafel" |
|
const match = link.getAttribute('href')?.match(/user\?id=([^&]+)/); |
|
if (!match) return; |
|
const username = match[1]; |
|
|
|
// Insert a badge span right after the username link |
|
const badge = makeBadge(username); |
|
link.after(badge); |
|
|
|
// Queue a fetch (deduped) |
|
if (!seen.has(username)) { |
|
seen.add(username); |
|
fetchUser(username); |
|
} |
|
}); |
|
} |
|
|
|
// Run on page load |
|
if (document.readyState === 'loading') { |
|
document.addEventListener('DOMContentLoaded', run); |
|
} else { |
|
run(); |
|
} |
|
})(); |