8chan Collapsible Thread Chains/Nested Inline Replies

Make quote links collapsible with indented hierarchy. Override panelBacklinks behavior.

As of 2025-04-19. See the latest version.

  1. // ==UserScript==
  2. // @name 8chan Collapsible Thread Chains/Nested Inline Replies
  3. // @version 1.5.9
  4. // @description Make quote links collapsible with indented hierarchy. Override panelBacklinks behavior.
  5. // @match https://8chan.moe/*/res/*
  6. // @match https://8chan.se/*/res/*
  7. // @grant GM_addStyle
  8. // @grant GM.addStyle
  9. // @license MIT
  10. // @namespace https://greatest.deepsurf.us/users/1459581
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. GM_addStyle(`
  17. .collapsible-container {
  18. margin-left: 20px;
  19. padding-left: 5px;
  20. margin-top: 8px;
  21. }
  22. .post-content.collapsed {
  23. display: none;
  24. }
  25. .altBacklinks {
  26. display: none !important;
  27. }
  28. .postCell.post-content {
  29. border: none !important;
  30. }
  31. .innerPost {
  32. border-top: 1px solid #474b53;
  33. border-left: 1px solid #474b53;
  34. width: auto;
  35. max-width: none !important;
  36. }
  37. `);
  38.  
  39. const linkContainers = new WeakMap();
  40.  
  41. function handlequickreply(event) {
  42. const link = event.target.closest('a');
  43. qr.showQr(link.href.match(/#q(\d+)/)[1]);
  44. }
  45.  
  46. function handleQuoteClick(event) {
  47. event.preventDefault();
  48. event.stopPropagation();
  49. const link = event.target.closest('a');
  50. if (!link) return;
  51.  
  52. const rawHash = link.hash.includes('?') ? link.hash.split('?')[0] : link.hash;
  53. const targetId = rawHash.substring(1).replace(/^q/, '');
  54. const targetPost = document.getElementById(targetId);
  55.  
  56. if (!targetPost) return;
  57.  
  58. let container = linkContainers.get(link);
  59. if (container) {
  60. const clone = container.querySelector('.post-content');
  61. clone.classList.toggle('collapsed');
  62. return;
  63. }
  64.  
  65. const level = link.closest('.collapsible-container')?.dataset.level || 0;
  66. container = document.createElement('div');
  67. container.className = 'collapsible-container';
  68. container.dataset.level = parseInt(level) + 1;
  69.  
  70. const clone = targetPost.cloneNode(true);
  71. clone.removeAttribute('id');
  72. clone.classList.add('post-content');
  73.  
  74. processClonedElements(clone);
  75.  
  76. container.appendChild(clone);
  77.  
  78. const postContainer = link.closest('.innerPost');
  79. if (postContainer) {
  80. postContainer.appendChild(container);
  81. } else {
  82. link.parentNode.insertBefore(container, link.nextSibling);
  83. }
  84.  
  85. linkContainers.set(link, container);
  86. }
  87.  
  88. function processClonedElements(clone) {
  89. clone.querySelectorAll('a.linkQuote, .panelBacklinks a').forEach(link => {
  90. const href = link.getAttribute('href');
  91. if (href && href.includes('#')) {
  92. const cleanHash = href.split('#')[1].split('?')[0];
  93. link.href = `#${cleanHash}`;
  94. }
  95. link.addEventListener('click', handleQuoteClick);
  96. });
  97.  
  98. const firstQuoteLink = clone.querySelector('a.linkQuote');
  99. if (firstQuoteLink) {
  100. firstQuoteLink.addEventListener('click', handlequickreply);
  101. }
  102. }
  103.  
  104. function initializeLinks() {
  105. document.querySelectorAll('a.linkQuote, .panelBacklinks a').forEach(link => {
  106. const href = link.getAttribute('href');
  107. if (href?.includes('#')) {
  108. link.href = `#${href.split('#')[1].split('?')[0]}`;
  109. }
  110. link.removeEventListener('click', handleQuoteClick);
  111. link.addEventListener('click', handleQuoteClick);
  112. });
  113.  
  114. // Add the panelBacklinks prevention
  115. document.querySelectorAll('span.panelBacklinks a').forEach(link => {
  116. link.addEventListener('click', function(e) {
  117. e.preventDefault();
  118. e.stopPropagation();
  119. });
  120. });
  121. }
  122.  
  123. initializeLinks();
  124.  
  125. new MutationObserver((mutations) => {
  126. mutations.forEach((mutation) => {
  127. if (mutation.addedNodes.length) {
  128. initializeLinks();
  129. }
  130. });
  131. }).observe(document.body, { childList: true, subtree: true });
  132. })();