by qarl | 2026-02-06 02:42:47+00:00
It's a shame that there's no way to block obnoxious people on HN.

I think I'll ask codex to write me a Chrome extension to censor you. :)

EDIT: Done!

(() => {
"use strict";

const BLOCKED_USER = "bopbopbop7";

// -------- helpers --------

/** True if the node (or its descendants) contain a user link matching blocked user */
function containsBlockedUserLink(root) {
if (!root || !(root instanceof Element)) return false;
const sel = `a[href^="user?id="], a[href*="user?id="]`;
const links = root.querySelectorAll(sel);
for (const a of links) {
const href = a.getAttribute("href") || "";
// HN uses relative href like "user?id=pg"
// Algolia uses full/relative links; check query portion.
if (href.includes(`user?id=${BLOCKED_USER}`)) return true;
if ((a.textContent || "").trim() === BLOCKED_USER && href.includes("user?id=")) return true;
}
return false;
}

/** Remove the closest HN listing block (table row pairs) for a story */
function removeHNStoryFromListing(anchorUserLink) {
// On HN listings, a story is typically 2 consecutive <tr>:
// 1) title row (class="athing"), 2) subtext row.
const userTr = anchorUserLink.closest("tr");
if (!userTr) return;

// If we are in the subtext row, the associated title row is usually previousElementSibling
// The title row has class "athing".
const maybeTitleTr = userTr.previousElementSibling;
const titleTr = maybeTitleTr && maybeTitleTr.classList && maybeTitleTr.classList.contains("athing")
? maybeTitleTr
: userTr.classList && userTr.classList.contains("athing")
? userTr
: null;

if (titleTr) {
const subtextTr = titleTr.nextElementSibling;
titleTr.remove();
if (subtextTr && subtextTr.tagName === "TR") subtextTr.remove();

// Sometimes there is a spacing row after; remove if it's just spacing
const spacer = (subtextTr && subtextTr.nextElementSibling) || null;
if (spacer && spacer.tagName === "TR" && spacer.textContent.trim() === "") {
spacer.remove();
}
} else {
// Fallback: remove just the row containing the user link
userTr.remove();
}
}

/** Collapse/remove comments by the blocked user on item pages */
function collapseHNComment(anchorUserLink) {
// HN comments are in <tr class="athing comtr"> (or similar)
const comtr = anchorUserLink.closest("tr.athing.comtr, tr.comtr");
if (!comtr) return;

// Remove the comment row plus the following "spacer" row if present
const next = comtr.nextElementSibling;
comtr.remove();
if (next && next.tagName === "TR" && next.textContent.trim() === "") {
next.remove();
}
}

/** Algolia results: remove the result item container */
function removeAlgoliaResult(anchorUserLink) {
// Algolia HN search commonly uses .item-title-and-infos or .Story or .Comment containers.
// We'll just walk up to a reasonable container.
const container =
anchorUserLink.closest("li, .item, .Story, .Comment, .SearchResults__item, .search-result, article, .result") ||
anchorUserLink.parentElement;

if (container) container.remove();
}

function handleUserLink(a) {
const href = a.getAttribute("href") || "";
const isBlocked =
href.includes(`user?id=${BLOCKED_USER}`) || (a.textContent || "").trim() === BLOCKED_USER;

if (!isBlocked) return;

const host = location.hostname;

if (host === "news.ycombinator.com") {
// Distinguish listing pages vs item pages by presence of "item?id="
const isItemPage = location.pathname === "/item";
if (isItemPage) {
collapseHNComment(a);
// Also on item page: if OP is blocked, remove the top submission block.
// We can try removing the whole "athing" story block if the subtext shows blocked user.
const topStoryTr = document.querySelector("tr.athing");
if (topStoryTr && containsBlockedUserLink(topStoryTr.nextElementSibling)) {
topStoryTr.remove();
const sub = topStoryTr.nextElementSibling;
if (sub && sub.tagName === "TR") sub.remove();
}
} else {
removeHNStoryFromListing(a);
}
} else if (host === "hn.algolia.com") {
removeAlgoliaResult(a);
}
}

function scanAndRemove(root = document) {
const links = root.querySelectorAll(`a[href*="user?id=${BLOCKED_USER}"]`);
for (const a of links) handleUserLink(a);

// Extra safety: sometimes the link text is present but href format differs.
// Catch those too.
const maybeTextLinks = root.querySelectorAll(`a`);
for (const a of maybeTextLinks) {
if ((a.textContent || "").trim() === BLOCKED_USER && (a.getAttribute("href") || "").includes("user?id=")) {
handleUserLink(a);
}
}
}

// Initial sweep
scanAndRemove(document);

// Keep it working with infinite scroll / dynamic loads
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const node of m.addedNodes) {
if (node instanceof Element) scanAndRemove(node);
}
}
});

obs.observe(document.documentElement, { childList: true, subtree: true });

})();