Filter 1PBTID (in-thread)

Hide low-post-count IDs inside /pol/ and /biz/ threads once the thread passes a post threshold; auto-unhide a poster once they exceed the per-ID limit; mask the filtered ID in the badge, quote previews, inline expansions and quotelinks (Requires 4chanX)

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name Filter 1PBTID (in-thread)
// @namespace Violentmonkey Scripts
// @match https://boards.4chan.org/*
// @match https://boards.4channel.org/*
// @grant none
// @version 2.3
// @run-at document-start
// @author Adapted from Some Anon on /g/ (https://desuarchive.org/g/thread/85972536/#q85996940)
// @description Hide low-post-count IDs inside /pol/ and /biz/ threads once the thread passes a post threshold; auto-unhide a poster once they exceed the per-ID limit; mask the filtered ID in the badge, quote previews, inline expansions and quotelinks (Requires 4chanX)
// @license MIT
// ==/UserScript==

const POST_THRESHOLD = 100;        // thread must have at least this many posts before filtering activates
const MAX_POSTS_PER_ID = 1;        // filter posters with this many posts or fewer (1 = 1pbtID, 2 = up to 2pbtID, etc.)
const BOARDS = ['pol', 'biz'];     // boards with poster IDs that this runs on
const HIDE_OP = false;             // set true to also hide the OP when OP is a filtered ID
const VERBOSE = false;             // set false to silence console logging
const HIDDEN_CLASS = 'onepbtid-hidden';
const REVEAL_CLASS = 'onepbtid-reveal';
const COUNTER_LABEL = '1pbtID Niggers filtered'; // label shown on the on-screen counter
const MASK_TEXT = 'user filtered'; // text shown in place of a filtered poster's ID and quotelinks to it
const MASK_COLOR = '#d00';         // color of the mask text
const RECHECK_DELAY = 250;         // ms debounce for re-evaluating after thread updates

const log = (...args) => { if (VERBOSE) console.log('[1pbtID]', ...args); };

const getBoard = () => {
 const m = location.href.match(/https:\/\/boards\.4chan(?:nel)?\.org\/([^/]+)\/thread\//);
 return m ? m[1] : null;
};

const getUid = container => {
 const el = container.querySelector('.posteruid');
 if (!el) return null;
 const m = el.className.match(/\bid_(\S+)/);
 return m ? m[1] : null;
};

const injectStyle = () => {
 if (document.getElementById('onepbtid-style')) return;
 const style = document.createElement('style');
 style.id = 'onepbtid-style';
 style.textContent = `
  .${HIDDEN_CLASS}{display:none !important;}
  body.${REVEAL_CLASS} .${HIDDEN_CLASS}{display:block !important;outline:2px solid #d00;}
  #onepbtid-counter{position:fixed;bottom:8px;left:8px;z-index:2147483647;
   font:12px/1.4 sans-serif;background:#1d1f21;color:#eee;padding:5px 8px;
   border:1px solid #444;border-radius:4px;cursor:pointer;opacity:.85;user-select:none;}
  #onepbtid-counter:hover{opacity:1;}
  #onepbtid-counter b{color:#ff6b6b;}
 `;
 (document.head || document.documentElement).appendChild(style);
};

// Replace the visible identity of filtered posters with MASK_TEXT wherever it renders:
//  - the poster ID badge (.posteruid .hand) on the post, quote previews and inline expansions
//  - any quotelink/backlink (>>postno) that points to a filtered post
// Driven purely by the id_<uid> class and the post number via href, so dynamically
// created previews/inlines/backlinks are covered without extra observers. Real
// identities are shown again while the thread is in reveal mode.
let maskStyleEl = null;
let maskSig = '';
const updateMaskStyle = (uids, nums) => {
 const uidList = [...uids].sort();
 const numList = [...nums].sort();
 const sig = uidList.join('|') + '#' + numList.join('|');
 if (sig === maskSig) return;
 maskSig = sig;
 if (!maskStyleEl) {
  maskStyleEl = document.createElement('style');
  maskStyleEl.id = 'onepbtid-mask-style';
  (document.head || document.documentElement).appendChild(maskStyleEl);
 }
 const rules = [];
 if (uidList.length) {
  const base = uidList.map(u => `body:not(.${REVEAL_CLASS}) .posteruid.${CSS.escape('id_' + u)} .hand`);
  rules.push(base.join(',') + '{font-size:0 !important;letter-spacing:0;}');
  rules.push(base.map(s => s + '::after').join(',') + `{content:'${MASK_TEXT}';font-size:12px;letter-spacing:normal;color:${MASK_COLOR} !important;}`);
 }
 if (numList.length) {
  const qbase = numList.map(n => `body:not(.${REVEAL_CLASS}) a.quotelink[href$="#p${n}"]`);
  rules.push(qbase.join(',') + '{font-size:0;letter-spacing:0;}');
  rules.push(qbase.map(s => s + '::after').join(',') + `{content:'>>${MASK_TEXT}';font-size:10pt;letter-spacing:normal;color:${MASK_COLOR} !important;}`);
 }
 maskStyleEl.textContent = rules.join('\n');
};

const buildCounter = () => {
 let el = document.getElementById('onepbtid-counter');
 if (el) return el;
 el = document.createElement('div');
 el.id = 'onepbtid-counter';
 el.title = 'Click to reveal/re-hide filtered posts';
 el.addEventListener('click', () => {
  const revealed = document.body.classList.toggle(REVEAL_CLASS);
  log(revealed ? 'Revealing filtered posts' : 'Re-hiding filtered posts');
 });
 document.body.appendChild(el);
 return el;
};

const updateCounter = (hidden, total, active) => {
 const el = buildCounter();
 const status = active ? `${total}/${POST_THRESHOLD}` : `${total}/${POST_THRESHOLD} (inactive)`;
 el.innerHTML = `${COUNTER_LABEL}: <b>${hidden}</b> &middot; posts ${status}`;
};

// Recompute counts every pass so a poster who exceeds MAX_POSTS_PER_ID is automatically unhidden.
const evaluate = thread => {
 const containers = [...thread.querySelectorAll(':scope > .postContainer')];
 const counts = new Map();
 const uids = new Map();
 for (const c of containers) {
  const uid = getUid(c);
  uids.set(c, uid);
  if (uid) counts.set(uid, (counts.get(uid) || 0) + 1);
 }
 const active = containers.length >= POST_THRESHOLD;
 const maskedUids = new Set();
 const maskedNums = new Set();
 let hidden = 0;
 let newlyHidden = 0;
 let newlyShown = 0;
 for (const c of containers) {
  const uid = uids.get(c);
  const isOp = c.classList.contains('opContainer');
  const hide = active && uid && counts.get(uid) <= MAX_POSTS_PER_ID && (HIDE_OP || !isOp);
  const was = c.classList.contains(HIDDEN_CLASS);
  if (hide && !was) { newlyHidden++; log(`Hiding post >>${c.id.replace(/^pc/, '')} (ID ${uid})`); }
  if (!hide && was) { newlyShown++; log(`Unhiding post >>${c.id.replace(/^pc/, '')} (ID ${uid})`); }
  c.classList.toggle(HIDDEN_CLASS, hide);
  if (hide) { hidden++; maskedUids.add(uid); maskedNums.add(c.id.replace(/^pc/, '')); }
 }
 updateMaskStyle(maskedUids, maskedNums);
 log(`Pass: ${containers.length} posts, active=${active}, filtered=${hidden} (+${newlyHidden}/-${newlyShown})`);
 updateCounter(hidden, containers.length, active);
};

const start = () => {
 const board = getBoard();
 if (!board || !BOARDS.includes(board)) return;
 const thread = document.querySelector('.thread');
 if (!thread) return;
 injectStyle();
 evaluate(thread);
 let timer = null;
 const recheck = () => {
  clearTimeout(timer);
  timer = setTimeout(() => evaluate(thread), RECHECK_DELAY);
 };
 new MutationObserver(recheck).observe(thread, { childList: true });
};

if (document.readyState === 'loading') {
 document.addEventListener('DOMContentLoaded', start);
} else {
 start();
}