hackernews account age and karma userscript

3 min read Original article ↗
// ==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(); } })();