Guest User

Blockland Forum User Blocker Tampermonkey

a guest
May 3rd, 2026
137
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name         Blockland Forum User Blocker
  3. // @namespace    http://tampermonkey.net/
  4. // @version      1.1
  5. // @description  Block and censor posts, topics, and board activity from specific users on the Blockland forums
  6. // @match        https://forum.blockland.us/*
  7. // @grant        GM_getValue
  8. // @grant        GM_setValue
  9. // @grant        GM_registerMenuCommand
  10. // ==/UserScript==
  11.  
  12. (function () {
  13.   'use strict';
  14.  
  15.   // --- Blocklist Management ---
  16.  
  17.   function getBlocklist() {
  18.     const raw = GM_getValue('blocklist', '[]');
  19.     try {
  20.       return JSON.parse(raw);
  21.     } catch {
  22.       return [];
  23.     }
  24.   }
  25.  
  26.   function saveBlocklist(list) {
  27.     GM_setValue('blocklist', JSON.stringify(list));
  28.   }
  29.  
  30.   function isBlocked(username) {
  31.     return getBlocklist().some((n) => n.toLowerCase() === username.toLowerCase());
  32.   }
  33.  
  34.   // --- Settings ---
  35.  
  36.   function getHideMode() {
  37.     return GM_getValue('hideMode', 'censor'); // 'censor' or 'hide'
  38.   }
  39.  
  40.   function setHideMode(mode) {
  41.     GM_setValue('hideMode', mode);
  42.   }
  43.  
  44.   // Generic avatar as a data URI (simple silhouette)
  45.   const GENERIC_AVATAR = 'data:image/svg+xml,' + encodeURIComponent(
  46.     '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" fill="#555" background="#333">' +
  47.     '<rect width="80" height="80" fill="#333"/>' +
  48.     '<circle cx="40" cy="28" r="14" fill="#555"/>' +
  49.     '<ellipse cx="40" cy="70" rx="24" ry="18" fill="#555"/>' +
  50.     '</svg>'
  51.   );
  52.  
  53.   function addToBlocklist(username) {
  54.     const current = getBlocklist();
  55.     if (!current.some((n) => n.toLowerCase() === username.toLowerCase())) {
  56.       current.push(username);
  57.       saveBlocklist(current);
  58.       runAll();
  59.     }
  60.   }
  61.  
  62.   // --- UI: Blocklist Editor Panel ---
  63.  
  64.   function createBlocklistPanel() {
  65.     if (document.getElementById('blf-blocklist-panel')) {
  66.       document.getElementById('blf-blocklist-panel').remove();
  67.       return;
  68.     }
  69.  
  70.     const overlay = document.createElement('div');
  71.     overlay.id = 'blf-blocklist-panel';
  72.     overlay.style.cssText = `
  73.       position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  74.       background: rgba(0,0,0,0.6); z-index: 99999;
  75.       display: flex; align-items: center; justify-content: center;
  76.     `;
  77.  
  78.     const panel = document.createElement('div');
  79.     panel.style.cssText = `
  80.       background: #e9eef2; color: #444; border: 1px solid #a9b7c0;
  81.       border-radius: 4px; padding: 20px; width: 400px; max-height: 80vh;
  82.       box-shadow: 0 4px 16px rgba(0,0,0,0.3);
  83.       font: 12px Verdana, Arial, Helvetica, sans-serif;
  84.       display: flex; flex-direction: column;
  85.     `;
  86.  
  87.     const title = document.createElement('h2');
  88.     title.textContent = 'Blocked Users';
  89.     title.style.cssText = 'margin: 0 0 12px 0; font-size: 16px; color: #346; font-family: Verdana, Arial, Helvetica, sans-serif;';
  90.     panel.appendChild(title);
  91.  
  92.     const listContainer = document.createElement('div');
  93.     listContainer.style.cssText = 'flex: 1; overflow-y: auto; margin-bottom: 12px; max-height: 300px;';
  94.  
  95.     function renderList() {
  96.       listContainer.innerHTML = '';
  97.       const current = getBlocklist();
  98.       if (current.length === 0) {
  99.         const empty = document.createElement('div');
  100.         empty.textContent = 'No blocked users yet. Add one below.';
  101.         empty.style.cssText = 'color: #888; font-size: 11px; padding: 10px 0;';
  102.         listContainer.appendChild(empty);
  103.       } else {
  104.         current.forEach((name) => {
  105.           const row = document.createElement('div');
  106.           row.style.cssText = `
  107.             display: flex; align-items: center; justify-content: space-between;
  108.             padding: 5px 8px; margin-bottom: 3px; background: #d4dae0; border-radius: 2px;
  109.           `;
  110.           const label = document.createElement('span');
  111.           label.textContent = name;
  112.           label.style.cssText = 'font-size: 12px; color: #444;';
  113.  
  114.           const removeBtn = document.createElement('button');
  115.           removeBtn.textContent = 'Unblock';
  116.           removeBtn.style.cssText = `
  117.             background: none; color: #346; border: none;
  118.             padding: 2px 0; cursor: pointer; font: 11px Verdana, Arial, Helvetica, sans-serif;
  119.             text-decoration: underline;
  120.           `;
  121.           removeBtn.addEventListener('click', () => {
  122.             const updated = getBlocklist().filter((n) => n !== name);
  123.             saveBlocklist(updated);
  124.             renderList();
  125.             runAll();
  126.           });
  127.  
  128.           row.appendChild(label);
  129.           row.appendChild(removeBtn);
  130.           listContainer.appendChild(row);
  131.         });
  132.       }
  133.     }
  134.     renderList();
  135.     panel.appendChild(listContainer);
  136.  
  137.     // Add user input
  138.     const addRow = document.createElement('div');
  139.     addRow.style.cssText = 'display: flex; gap: 8px;';
  140.  
  141.     const input = document.createElement('input');
  142.     input.type = 'text';
  143.     input.placeholder = 'Enter username to block...';
  144.     input.style.cssText = `
  145.       flex: 1; padding: 4px 8px; border-radius: 2px; border: 1px solid #a9b7c0;
  146.       background: #fff; color: #444; font: 12px Verdana, Arial, Helvetica, sans-serif;
  147.     `;
  148.  
  149.     const addBtn = document.createElement('button');
  150.     addBtn.textContent = 'Block';
  151.     addBtn.style.cssText = `
  152.       background: #d4dae0; color: #346; border: 1px solid #a9b7c0; border-radius: 2px;
  153.       padding: 4px 12px; cursor: pointer; font: 12px Verdana, Arial, Helvetica, sans-serif;
  154.     `;
  155.  
  156.     function addUser() {
  157.       const name = input.value.trim();
  158.       if (!name) return;
  159.       addToBlocklist(name);
  160.       renderList();
  161.       input.value = '';
  162.     }
  163.  
  164.     addBtn.addEventListener('click', addUser);
  165.     input.addEventListener('keydown', (e) => {
  166.       if (e.key === 'Enter') addUser();
  167.     });
  168.  
  169.     addRow.appendChild(input);
  170.     addRow.appendChild(addBtn);
  171.     panel.appendChild(addRow);
  172.  
  173.     // Hide mode setting
  174.     const settingsRow = document.createElement('div');
  175.     settingsRow.style.cssText = 'margin-top: 14px; padding-top: 12px; border-top: 1px solid #a9b7c0;';
  176.  
  177.     const settingsLabel = document.createElement('div');
  178.     settingsLabel.textContent = 'Block Mode';
  179.     settingsLabel.style.cssText = 'font-size: 12px; font-weight: bold; margin-bottom: 6px; color: #346;';
  180.     settingsRow.appendChild(settingsLabel);
  181.  
  182.     const currentMode = getHideMode();
  183.  
  184.     const censorRadio = document.createElement('input');
  185.     censorRadio.type = 'radio';
  186.     censorRadio.name = 'blf-hide-mode';
  187.     censorRadio.value = 'censor';
  188.     censorRadio.id = 'blf-mode-censor';
  189.     censorRadio.checked = currentMode === 'censor';
  190.  
  191.     const censorLabel = document.createElement('label');
  192.     censorLabel.htmlFor = 'blf-mode-censor';
  193.     censorLabel.textContent = ' Censor & anonymize (show placeholder)';
  194.     censorLabel.style.cssText = 'font-size: 11px; cursor: pointer; color: #444;';
  195.  
  196.     const hideRadio = document.createElement('input');
  197.     hideRadio.type = 'radio';
  198.     hideRadio.name = 'blf-hide-mode';
  199.     hideRadio.value = 'hide';
  200.     hideRadio.id = 'blf-mode-hide';
  201.     hideRadio.checked = currentMode === 'hide';
  202.  
  203.     const hideLabel = document.createElement('label');
  204.     hideLabel.htmlFor = 'blf-mode-hide';
  205.     hideLabel.textContent = ' Completely hide (remove from page)';
  206.     hideLabel.style.cssText = 'font-size: 11px; cursor: pointer; color: #444;';
  207.  
  208.     const row1 = document.createElement('div');
  209.     row1.style.marginBottom = '4px';
  210.     row1.appendChild(censorRadio);
  211.     row1.appendChild(censorLabel);
  212.  
  213.     const row2 = document.createElement('div');
  214.     row2.appendChild(hideRadio);
  215.     row2.appendChild(hideLabel);
  216.  
  217.     settingsRow.appendChild(row1);
  218.     settingsRow.appendChild(row2);
  219.  
  220.     function onModeChange() {
  221.       setHideMode(this.value);
  222.       runAll();
  223.     }
  224.     censorRadio.addEventListener('change', onModeChange);
  225.     hideRadio.addEventListener('change', onModeChange);
  226.  
  227.     panel.appendChild(settingsRow);
  228.  
  229.     // Close button
  230.     const closeBtn = document.createElement('button');
  231.     closeBtn.textContent = 'Close';
  232.     closeBtn.style.cssText = `
  233.       margin-top: 12px; background: #d4dae0; color: #346; border: 1px solid #a9b7c0;
  234.       border-radius: 2px; padding: 6px; cursor: pointer;
  235.       font: 12px Verdana, Arial, Helvetica, sans-serif;
  236.     `;
  237.     closeBtn.addEventListener('click', () => overlay.remove());
  238.     panel.appendChild(closeBtn);
  239.  
  240.     overlay.appendChild(panel);
  241.     overlay.addEventListener('click', (e) => {
  242.       if (e.target === overlay) overlay.remove();
  243.     });
  244.     document.body.appendChild(overlay);
  245.   }
  246.  
  247.   // --- Inject CSS ---
  248.  
  249.   const style = document.createElement('style');
  250.   style.textContent = `
  251.     /* === Censored posts (topic view) === */
  252.     .blf-censored .poster h4 a,
  253.     .blf-censored .poster h4 {
  254.       color: #666 !important;
  255.       font-style: italic;
  256.     }
  257.     /* avatar is replaced via JS with a generic silhouette */
  258.  
  259.     /* === Completely hidden mode === */
  260.     div.windowbg.blf-hidden,
  261.     div.windowbg2.blf-hidden {
  262.       display: none !important;
  263.     }
  264.     tr.blf-topic-hidden {
  265.       display: none !important;
  266.     }
  267.     .blf-censored .post .inner {
  268.       position: relative;
  269.       color: transparent !important;
  270.       user-select: none;
  271.     }
  272.     .blf-censored .post .inner * {
  273.       color: transparent !important;
  274.       visibility: hidden;
  275.     }
  276.     .blf-censored .post .inner::after {
  277.       content: 'This post has been hidden because the user is on your blocklist.';
  278.       position: absolute;
  279.       top: 0; left: 0; right: 0; bottom: 0;
  280.       display: flex; align-items: center; justify-content: center;
  281.       color: #888;
  282.       font-style: italic;
  283.       font-size: 13px;
  284.       background: repeating-linear-gradient(
  285.         45deg,
  286.         transparent,
  287.         transparent 10px,
  288.         rgba(100,100,100,0.08) 10px,
  289.         rgba(100,100,100,0.08) 20px
  290.       );
  291.       visibility: visible;
  292.     }
  293.     .blf-censored .post .inner img,
  294.     .blf-censored .post .inner iframe,
  295.     .blf-censored .post .inner video,
  296.     .blf-censored .post .inner embed {
  297.       display: none !important;
  298.     }
  299.     .blf-censored .postarea .keyinfo {
  300.       visibility: visible;
  301.     }
  302.  
  303.     /* === Censored topic rows (board view) === */
  304.     tr.blf-topic-censored td {
  305.       opacity: 0.4;
  306.     }
  307.     tr.blf-topic-censored td.subject span a {
  308.       color: #666 !important;
  309.     }
  310.     tr.blf-topic-censored td.subject p {
  311.       font-style: italic;
  312.       color: #666 !important;
  313.     }
  314.     tr.blf-topic-censored td.subject p a {
  315.       color: #666 !important;
  316.     }
  317.  
  318.     /* === Censored "last post by" on board index and topic listing === */
  319.     .blf-lastpost-censored {
  320.       font-style: italic;
  321.       opacity: 0.4;
  322.     }
  323.     .blf-lastpost-censored a {
  324.       color: #666 !important;
  325.     }
  326.  
  327.     /* === Censored quotes === */
  328.     .quoteheader.blf-quote-censored div a {
  329.       color: #666 !important;
  330.       font-style: italic;
  331.     }
  332.     blockquote.blf-quote-censored {
  333.       position: relative;
  334.       color: transparent !important;
  335.       user-select: none;
  336.     }
  337.     blockquote.blf-quote-censored * {
  338.       color: transparent !important;
  339.       visibility: hidden;
  340.     }
  341.     blockquote.blf-quote-censored img,
  342.     blockquote.blf-quote-censored iframe,
  343.     blockquote.blf-quote-censored video,
  344.     blockquote.blf-quote-censored embed {
  345.       display: none !important;
  346.     }
  347.     blockquote.blf-quote-censored::after {
  348.       content: 'Quoted content from a blocked user has been hidden.';
  349.       position: absolute;
  350.       top: 0; left: 0; right: 0; bottom: 0;
  351.       display: flex; align-items: center; justify-content: center;
  352.       color: #888;
  353.       font: italic 11px Verdana, Arial, Helvetica, sans-serif;
  354.       visibility: visible;
  355.     }
  356.  
  357.     /* Show/hide toggle button — styled as a native forum smalltext link */
  358.     .blf-show-btn {
  359.       background: none; border: none;
  360.       color: #346; cursor: pointer;
  361.       font: 11px Verdana, Arial, Helvetica, sans-serif;
  362.       margin-left: 6px; padding: 0;
  363.       text-decoration: underline;
  364.     }
  365.     .blf-show-btn:hover { color: #c06002; }
  366.  
  367.     /* Block user link in poster sidebar — matches forum smalltext links */
  368.     .blf-block-link {
  369.       cursor: pointer;
  370.       color: #346;
  371.       font: 10px Verdana, Arial, Helvetica, sans-serif;
  372.       display: block; margin-top: 4px;
  373.       margin-left: 16px;
  374.     }
  375.     .blf-block-link:hover { color: #c06002; text-decoration: underline; }
  376.  
  377.     /* Inline topic row show button — same smalltext link style */
  378.     .blf-topic-show-btn {
  379.       background: none; border: none;
  380.       color: #346; cursor: pointer;
  381.       font: 10px Verdana, Arial, Helvetica, sans-serif;
  382.       margin-left: 6px; padding: 0;
  383.       text-decoration: underline;
  384.       vertical-align: middle;
  385.     }
  386.     .blf-topic-show-btn:hover { color: #c06002; }
  387.   `;
  388.   document.head.appendChild(style);
  389.  
  390.   // =============================================
  391.   // 1) TOPIC VIEW: Filter/censor individual posts
  392.   // =============================================
  393.  
  394.   function filterPosts() {
  395.     const posts = document.querySelectorAll('div.windowbg, div.windowbg2');
  396.     if (posts.length === 0) return;
  397.     const mode = getHideMode();
  398.  
  399.     posts.forEach((post) => {
  400.       const h4 = post.querySelector('.poster h4');
  401.       if (!h4) return;
  402.  
  403.       // Store original name and avatar once
  404.       if (!post.dataset.blfOriginalName) {
  405.         post.dataset.blfOriginalName = h4.textContent.trim();
  406.       }
  407.       const avatarImg = post.querySelector('.poster img.avatar');
  408.       if (avatarImg && !post.dataset.blfOriginalAvatar) {
  409.         post.dataset.blfOriginalAvatar = avatarImg.src;
  410.       }
  411.  
  412.       const originalName = post.dataset.blfOriginalName;
  413.       const blocked = isBlocked(originalName);
  414.  
  415.       // Remove old show buttons
  416.       post.querySelectorAll('.blf-show-btn').forEach((b) => b.remove());
  417.  
  418.       if (blocked) {
  419.         if (mode === 'hide') {
  420.           // Completely hide the post
  421.           post.classList.remove('blf-censored');
  422.           post.classList.add('blf-hidden');
  423.           // Restore originals in case switching from censor mode
  424.           restorePost(post, h4, avatarImg);
  425.         } else {
  426.           // Censor mode
  427.           post.classList.remove('blf-hidden');
  428.           post.classList.add('blf-censored');
  429.  
  430.           // Replace username display
  431.           const link = h4.querySelector('a');
  432.           if (link) {
  433.             if (!link.dataset.blfOriginalText) link.dataset.blfOriginalText = link.textContent;
  434.             link.textContent = 'Blocked User';
  435.           } else {
  436.             if (!h4.dataset.blfOriginalText) h4.dataset.blfOriginalText = h4.textContent;
  437.             h4.textContent = 'Blocked User';
  438.           }
  439.  
  440.           // Replace avatar with generic silhouette, match original size
  441.           if (avatarImg) {
  442.             if (!avatarImg.dataset.blfOriginalWidth) {
  443.               avatarImg.dataset.blfOriginalWidth = avatarImg.width || 75;
  444.               avatarImg.dataset.blfOriginalHeight = avatarImg.height || 75;
  445.             }
  446.             avatarImg.src = GENERIC_AVATAR;
  447.             avatarImg.style.width = avatarImg.dataset.blfOriginalWidth + 'px';
  448.             avatarImg.style.height = avatarImg.dataset.blfOriginalHeight + 'px';
  449.           }
  450.  
  451.           // Add show/hide toggle
  452.           const keyinfo = post.querySelector('.keyinfo');
  453.           if (keyinfo) {
  454.             const showBtn = document.createElement('button');
  455.             showBtn.className = 'blf-show-btn';
  456.             showBtn.textContent = 'Show Post';
  457.             showBtn.addEventListener('click', () => {
  458.               if (post.classList.contains('blf-censored')) {
  459.                 post.classList.remove('blf-censored');
  460.                 restorePost(post, h4, avatarImg);
  461.                 showBtn.textContent = 'Hide Post';
  462.               } else {
  463.                 post.classList.add('blf-censored');
  464.                 const a = h4.querySelector('a');
  465.                 if (a) a.textContent = 'Blocked User';
  466.                 else h4.textContent = 'Blocked User';
  467.                 if (avatarImg) {
  468.                   avatarImg.src = GENERIC_AVATAR;
  469.                   avatarImg.style.width = (avatarImg.dataset.blfOriginalWidth || 75) + 'px';
  470.                   avatarImg.style.height = (avatarImg.dataset.blfOriginalHeight || 75) + 'px';
  471.                 }
  472.                 showBtn.textContent = 'Show Post';
  473.               }
  474.             });
  475.             keyinfo.appendChild(showBtn);
  476.           }
  477.         }
  478.       } else {
  479.         post.classList.remove('blf-censored');
  480.         post.classList.remove('blf-hidden');
  481.         restorePost(post, h4, avatarImg);
  482.       }
  483.     });
  484.   }
  485.  
  486.   function restorePost(post, h4, avatarImg) {
  487.     const link = h4.querySelector('a');
  488.     if (link && link.dataset.blfOriginalText) link.textContent = link.dataset.blfOriginalText;
  489.     else if (h4.dataset.blfOriginalText) h4.textContent = h4.dataset.blfOriginalText;
  490.     if (avatarImg && post.dataset.blfOriginalAvatar) {
  491.       avatarImg.src = post.dataset.blfOriginalAvatar;
  492.       avatarImg.style.width = '';
  493.       avatarImg.style.height = '';
  494.     }
  495.   }
  496.  
  497.   // Add "Block User" links under each poster in topic view
  498.   function addBlockLinks() {
  499.     const posts = document.querySelectorAll('div.windowbg, div.windowbg2');
  500.     posts.forEach((post) => {
  501.       const poster = post.querySelector('.poster');
  502.       if (!poster || poster.querySelector('.blf-block-link')) return;
  503.  
  504.       const h4 = poster.querySelector('h4');
  505.       if (!h4) return;
  506.  
  507.       const blockLink = document.createElement('span');
  508.       blockLink.className = 'blf-block-link';
  509.       blockLink.textContent = 'Block User';
  510.       blockLink.addEventListener('click', () => {
  511.         const originalName = post.dataset.blfOriginalName || h4.textContent.trim();
  512.         addToBlocklist(originalName);
  513.       });
  514.       poster.appendChild(blockLink);
  515.     });
  516.   }
  517.  
  518.   // ==============================================
  519.   // 2) BOARD VIEW: Filter/censor topics in listing
  520.   // ==============================================
  521.   // Structure: table.table_grid > tr > td.subject
  522.   //   td.subject > div > span (topic link) + p ("Started by" <a>username</a>)
  523.  
  524.   function filterTopics() {
  525.     const table = document.querySelector('table.table_grid');
  526.     if (!table) return;
  527.     const mode = getHideMode();
  528.  
  529.     const rows = table.querySelectorAll('tbody tr');
  530.     rows.forEach((row) => {
  531.       const subjectCell = row.querySelector('td.subject');
  532.       if (!subjectCell) return;
  533.  
  534.       const startedByP = subjectCell.querySelector('p');
  535.       if (!startedByP) return;
  536.  
  537.       // Find the "Started by" author link
  538.       const authorLink = startedByP.querySelector('a');
  539.       if (!authorLink) return;
  540.  
  541.       // Only match the first link (the author), not page number links inside <small>
  542.       const small = startedByP.querySelector('small');
  543.       if (small && small.contains(authorLink)) return;
  544.       // Get first <a> that is a direct child or not inside <small>
  545.       let realAuthorLink = null;
  546.       for (const a of startedByP.querySelectorAll('a')) {
  547.         if (!small || !small.contains(a)) {
  548.           realAuthorLink = a;
  549.           break;
  550.         }
  551.       }
  552.       if (!realAuthorLink) return;
  553.  
  554.       if (!row.dataset.blfOriginalAuthor) {
  555.         row.dataset.blfOriginalAuthor = realAuthorLink.textContent.trim();
  556.       }
  557.  
  558.       const originalAuthor = row.dataset.blfOriginalAuthor;
  559.       const blocked = isBlocked(originalAuthor);
  560.  
  561.       // Remove old toggle buttons
  562.       row.querySelectorAll('.blf-topic-show-btn').forEach((b) => b.remove());
  563.  
  564.       if (blocked) {
  565.         if (mode === 'hide') {
  566.           row.classList.remove('blf-topic-censored');
  567.           row.classList.add('blf-topic-hidden');
  568.           if (realAuthorLink.dataset.blfOriginalText) {
  569.             realAuthorLink.textContent = realAuthorLink.dataset.blfOriginalText;
  570.           }
  571.         } else {
  572.           row.classList.remove('blf-topic-hidden');
  573.           row.classList.add('blf-topic-censored');
  574.  
  575.           if (!realAuthorLink.dataset.blfOriginalText) {
  576.             realAuthorLink.dataset.blfOriginalText = realAuthorLink.textContent;
  577.           }
  578.           realAuthorLink.textContent = 'Blocked User';
  579.  
  580.           // Add a small show toggle
  581.           const showBtn = document.createElement('button');
  582.           showBtn.className = 'blf-topic-show-btn';
  583.           showBtn.textContent = 'Show';
  584.           showBtn.addEventListener('click', (e) => {
  585.             e.preventDefault();
  586.             e.stopPropagation();
  587.             if (row.classList.contains('blf-topic-censored')) {
  588.               row.classList.remove('blf-topic-censored');
  589.               if (realAuthorLink.dataset.blfOriginalText) {
  590.                 realAuthorLink.textContent = realAuthorLink.dataset.blfOriginalText;
  591.               }
  592.               showBtn.textContent = 'Hide';
  593.             } else {
  594.               row.classList.add('blf-topic-censored');
  595.               realAuthorLink.textContent = 'Blocked User';
  596.               showBtn.textContent = 'Show';
  597.             }
  598.           });
  599.           startedByP.appendChild(showBtn);
  600.         }
  601.       } else {
  602.         row.classList.remove('blf-topic-censored');
  603.         row.classList.remove('blf-topic-hidden');
  604.         if (realAuthorLink.dataset.blfOriginalText) {
  605.           realAuthorLink.textContent = realAuthorLink.dataset.blfOriginalText;
  606.         }
  607.       }
  608.     });
  609.   }
  610.  
  611.   // =====================================================
  612.   // 3) BOARD INDEX: Censor "Last post by" blocked users
  613.   // =====================================================
  614.   // Structure: td.lastpost > p > "Last post" <strong> ... "by" <a>username</a> "in" <a>topic</a>
  615.  
  616.   function filterBoardIndex() {
  617.     const lastpostCells = document.querySelectorAll('td.lastpost');
  618.     if (lastpostCells.length === 0) return;
  619.     const mode = getHideMode();
  620.  
  621.     lastpostCells.forEach((cell) => {
  622.       const p = cell.querySelector('p');
  623.       if (!p) return;
  624.  
  625.       // Find the "by" author link — it's the <a> right after text containing "by"
  626.       let authorLink = null;
  627.       const childNodes = Array.from(p.childNodes);
  628.       for (let i = 0; i < childNodes.length; i++) {
  629.         const node = childNodes[i];
  630.         if (node.nodeType === 3 && node.textContent.includes('by')) {
  631.           // Next sibling element should be the author link
  632.           const next = childNodes[i + 1];
  633.           if (next && next.tagName === 'A') {
  634.             authorLink = next;
  635.             break;
  636.           }
  637.         }
  638.       }
  639.  
  640.       if (!authorLink) return;
  641.  
  642.       if (!cell.dataset.blfOriginalAuthor) {
  643.         cell.dataset.blfOriginalAuthor = authorLink.textContent.trim();
  644.       }
  645.  
  646.       const originalAuthor = cell.dataset.blfOriginalAuthor;
  647.       const blocked = isBlocked(originalAuthor);
  648.  
  649.       if (blocked) {
  650.         if (mode === 'hide') {
  651.           p.classList.add('blf-lastpost-censored');
  652.           if (!authorLink.dataset.blfOriginalText) {
  653.             authorLink.dataset.blfOriginalText = authorLink.textContent;
  654.           }
  655.           authorLink.textContent = 'Blocked User';
  656.         } else {
  657.           p.classList.add('blf-lastpost-censored');
  658.           if (!authorLink.dataset.blfOriginalText) {
  659.             authorLink.dataset.blfOriginalText = authorLink.textContent;
  660.           }
  661.           authorLink.textContent = 'Blocked User';
  662.         }
  663.       } else {
  664.         p.classList.remove('blf-lastpost-censored');
  665.         if (authorLink.dataset.blfOriginalText) {
  666.           authorLink.textContent = authorLink.dataset.blfOriginalText;
  667.         }
  668.       }
  669.     });
  670.   }
  671.  
  672.   // =====================================================
  673.   // 4) TOPIC LISTING: Censor "last post by" in td.lastpost
  674.   // =====================================================
  675.   // Structure on board pages: td.lastpost > ... text "by" <a>username</a> (no <p> wrapper)
  676.  
  677.   function filterTopicLastPosts() {
  678.     const table = document.querySelector('table.table_grid');
  679.     if (!table) return;
  680.  
  681.     const lastpostCells = table.querySelectorAll('td.lastpost');
  682.     lastpostCells.forEach((cell) => {
  683.       // Skip if already handled by filterBoardIndex (has <p> wrapper)
  684.       if (cell.querySelector('p')) return;
  685.  
  686.       let authorLink = null;
  687.       const childNodes = Array.from(cell.childNodes);
  688.       for (let i = 0; i < childNodes.length; i++) {
  689.         const node = childNodes[i];
  690.         if (node.nodeType === 3 && node.textContent.includes('by')) {
  691.           const next = childNodes[i + 1];
  692.           if (next && next.tagName === 'A') {
  693.             authorLink = next;
  694.             break;
  695.           }
  696.         }
  697.       }
  698.  
  699.       if (!authorLink) return;
  700.  
  701.       if (!cell.dataset.blfOriginalAuthor) {
  702.         cell.dataset.blfOriginalAuthor = authorLink.textContent.trim();
  703.       }
  704.  
  705.       const originalAuthor = cell.dataset.blfOriginalAuthor;
  706.       const blocked = isBlocked(originalAuthor);
  707.  
  708.       if (blocked) {
  709.         cell.classList.add('blf-lastpost-censored');
  710.         if (!authorLink.dataset.blfOriginalText) {
  711.           authorLink.dataset.blfOriginalText = authorLink.textContent;
  712.         }
  713.         authorLink.textContent = 'Blocked User';
  714.       } else {
  715.         cell.classList.remove('blf-lastpost-censored');
  716.         if (authorLink.dataset.blfOriginalText) {
  717.           authorLink.textContent = authorLink.dataset.blfOriginalText;
  718.         }
  719.       }
  720.     });
  721.   }
  722.  
  723.   // =====================================================
  724.   // 5) QUOTES: Censor quotes from blocked users
  725.   // =====================================================
  726.   // Structure: .quoteheader > div > a with text "Quote from: USERNAME on DATE"
  727.   //            followed by blockquote.bbc_standard_quote
  728.  
  729.   function filterQuotes() {
  730.     const quoteHeaders = document.querySelectorAll('.quoteheader');
  731.     if (quoteHeaders.length === 0) return;
  732.     const mode = getHideMode();
  733.  
  734.     quoteHeaders.forEach((qh) => {
  735.       const div = qh.querySelector('div');
  736.       if (!div) return;
  737.  
  738.       const link = div.querySelector('a');
  739.       // Use stored original or current text to extract username
  740.       const text = (link && link.dataset.blfOriginalText)
  741.         ? link.dataset.blfOriginalText
  742.         : (link ? link.textContent.trim() : div.textContent.trim());
  743.  
  744.       // Extract username from "Quote from: USERNAME on DATE"
  745.       const match = text.match(/^Quote from:\s+(.+?)\s+on\s+/);
  746.       if (!match) return;
  747.  
  748.       const quotedUser = match[1];
  749.  
  750.       // Store original link text once
  751.       if (link && !link.dataset.blfOriginalText) {
  752.         link.dataset.blfOriginalText = link.textContent;
  753.       }
  754.  
  755.       const blockquote = qh.nextElementSibling;
  756.       const isQuoteBq = blockquote && blockquote.classList.contains('bbc_standard_quote');
  757.       const blocked = isBlocked(quotedUser);
  758.  
  759.       if (blocked) {
  760.         if (mode === 'hide') {
  761.           qh.style.display = 'none';
  762.           if (isQuoteBq) blockquote.style.display = 'none';
  763.           qh.classList.remove('blf-quote-censored');
  764.           if (isQuoteBq) blockquote.classList.remove('blf-quote-censored');
  765.         } else {
  766.           qh.style.display = '';
  767.           if (isQuoteBq) blockquote.style.display = '';
  768.           qh.classList.add('blf-quote-censored');
  769.           if (isQuoteBq) blockquote.classList.add('blf-quote-censored');
  770.           if (link) link.textContent = 'Quote from: Blocked User';
  771.         }
  772.       } else {
  773.         qh.style.display = '';
  774.         if (isQuoteBq) blockquote.style.display = '';
  775.         qh.classList.remove('blf-quote-censored');
  776.         if (isQuoteBq) blockquote.classList.remove('blf-quote-censored');
  777.         if (link && link.dataset.blfOriginalText) {
  778.           link.textContent = link.dataset.blfOriginalText;
  779.         }
  780.       }
  781.     });
  782.   }
  783.  
  784.   // --- Tampermonkey Menu Command ---
  785.  
  786.   GM_registerMenuCommand('Edit Blocklist', createBlocklistPanel);
  787.  
  788.   // --- Add a [Blocklist] button to the forum nav ---
  789.  
  790.   function addNavButton() {
  791.     const dropmenu = document.querySelector('#main_menu .dropmenu');
  792.     if (dropmenu && !dropmenu.querySelector('.blf-nav-btn')) {
  793.       // Build the same structure as native nav items: li > a.firstlevel > span.firstlevel
  794.       const li = document.createElement('li');
  795.       const btn = document.createElement('a');
  796.       btn.className = 'firstlevel blf-nav-btn';
  797.       btn.href = '#';
  798.       const span = document.createElement('span');
  799.       span.className = 'firstlevel';
  800.       span.textContent = 'Blocklist';
  801.       btn.appendChild(span);
  802.       btn.addEventListener('click', (e) => {
  803.         e.preventDefault();
  804.         createBlocklistPanel();
  805.       });
  806.       li.appendChild(btn);
  807.       // Insert before the last direct child nav item (Logout/Register)
  808.       const directLis = Array.from(dropmenu.children).filter((c) => c.tagName === 'LI');
  809.       const lastLi = directLis[directLis.length - 1];
  810.       if (lastLi) {
  811.         dropmenu.insertBefore(li, lastLi);
  812.       } else {
  813.         dropmenu.appendChild(li);
  814.       }
  815.       return;
  816.     }
  817.     // Fallback: navigate_section breadcrumb area
  818.     const navSec = document.querySelector('.navigate_section');
  819.     if (navSec && !navSec.querySelector('.blf-nav-btn')) {
  820.       const btn = document.createElement('a');
  821.       btn.className = 'blf-nav-btn';
  822.       btn.textContent = 'Blocklist';
  823.       btn.href = '#';
  824.       btn.style.cssText = 'margin: 0 8px 0 0;';
  825.       btn.addEventListener('click', (e) => {
  826.         e.preventDefault();
  827.         createBlocklistPanel();
  828.       });
  829.       navSec.appendChild(btn);
  830.     }
  831.   }
  832.  
  833.   // --- Run all filters ---
  834.  
  835.   function runAll() {
  836.     filterPosts();
  837.     addBlockLinks();
  838.     filterQuotes();
  839.     filterTopics();
  840.     filterTopicLastPosts();
  841.     filterBoardIndex();
  842.   }
  843.  
  844.   // --- Init ---
  845.  
  846.   addNavButton();
  847.   runAll();
  848. })();
  849.  
Add Comment
Please, Sign In to add comment