// ==UserScript==
// @name 8chan Lightweight Extended Suite
// @namespace https://greasyfork.org/en/scripts/533173
// @version 2.1.8
// @description Spoiler revealer for 8chan with nested replies
// @author impregnator
// @match https://8chan.moe/*
// @match https://8chan.se/*
// @match https://8chan.cc/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Function to process images and replace spoiler placeholders with thumbnails
function processImages(images, isCatalog = false) {
images.forEach(img => {
// Check if the image is a spoiler placeholder (custom or default)
if (img.src.includes('custom.spoiler') || img.src.includes('spoiler.png')) {
let fullFileUrl;
if (isCatalog) {
// Catalog: Get the href from the parent <a class="linkThumb">
const link = img.closest('a.linkThumb');
if (link) {
// Construct the thumbnail URL based on the thread URL
fullFileUrl = link.href;
const threadMatch = fullFileUrl.match(/\/([a-z0-9]+)\/res\/([0-9]+)\.html$/i);
if (threadMatch && threadMatch[1] && threadMatch[2]) {
const board = threadMatch[1];
const threadId = threadMatch[2];
// Fetch the thread page to find the actual image URL
fetchThreadImage(board, threadId).then(thumbnailUrl => {
if (thumbnailUrl) {
img.src = thumbnailUrl;
}
});
}
}
} else {
// Thread: Get the parent <a> element containing the full-sized file URL
const link = img.closest('a.imgLink');
if (link) {
// Extract the full-sized file URL
fullFileUrl = link.href;
// Extract the file hash (everything after /.media/ up to the extension)
const fileHash = fullFileUrl.match(/\/\.media\/([a-f0-9]+)\.[a-z0-9]+$/i);
if (fileHash && fileHash[1]) {
// Construct the thumbnail URL using the current domain
const thumbnailUrl = `${window.location.origin}/.media/t_${fileHash[1]}`;
// Replace the spoiler image with the thumbnail
img.src = thumbnailUrl;
}
}
}
}
});
}
// Function to fetch the thread page and extract the thumbnail URL
async function fetchThreadImage(board, threadId) {
try {
const response = await fetch(`https://${window.location.host}/${board}/res/${threadId}.html`);
const text = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
// Find the first image in the thread's OP post
const imgLink = doc.querySelector('.uploadCell a.imgLink');
if (imgLink) {
const fullFileUrl = imgLink.href;
const fileHash = fullFileUrl.match(/\/\.media\/([a-f0-9]+)\.[a-z0-9]+$/i);
if (fileHash && fileHash[1]) {
return `${window.location.origin}/.media/t_${fileHash[1]}`;
}
}
return null;
} catch (error) {
console.error('Error fetching thread image:', error);
return null;
}
}
// Process existing images on page load
const isCatalogPage = window.location.pathname.includes('catalog.html');
if (isCatalogPage) {
const initialCatalogImages = document.querySelectorAll('.catalogCell a.linkThumb img');
processImages(initialCatalogImages, true);
} else {
const initialThreadImages = document.querySelectorAll('.uploadCell img');
processImages(initialThreadImages, false);
}
// Set up MutationObserver to handle dynamically added posts
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.addedNodes.length) {
// Check each added node for new images
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (isCatalogPage) {
const newCatalogImages = node.querySelectorAll('.catalogCell a.linkThumb img');
processImages(newCatalogImages, true);
} else {
const newThreadImages = node.querySelectorAll('.uploadCell img');
processImages(newThreadImages, false);
}
}
});
}
});
});
// Observe changes to the document body, including child nodes and subtrees
observer.observe(document.body, {
childList: true,
subtree: true
});
})();
//Opening all posts from the catalog in a new tag section
// Add click event listener to catalog thumbnail images
document.addEventListener('click', function(e) {
// Check if the clicked element is an image inside a catalog cell
if (e.target.tagName === 'IMG' && e.target.closest('.catalogCell')) {
// Find the parent link with class 'linkThumb'
const link = e.target.closest('.linkThumb');
if (link) {
// Prevent default link behavior
e.preventDefault();
// Open the thread in a new tab
window.open(link.href, '_blank');
}
}
});
//Automatically redirect to catalog section
// Redirect to catalog if on a board's main page, excluding overboard pages
(function() {
const currentPath = window.location.pathname;
// Check if the path matches a board's main page (e.g., /v/, /a/) but not overboard pages
if (currentPath.match(/^\/[a-zA-Z0-9]+\/$/) && !currentPath.match(/^\/(sfw|overboard)\/$/)) {
// Redirect to the catalog page
window.location.replace(currentPath + 'catalog.html');
}
})();
// Text spoiler revealer
(function() {
// Function to reveal spoilers
function revealSpoilers() {
const spoilers = document.querySelectorAll('span.spoiler');
spoilers.forEach(spoiler => {
// Override default spoiler styles to make text visible
spoiler.style.background = 'none';
spoiler.style.color = 'inherit';
spoiler.style.textShadow = 'none';
});
}
// Run initially for existing spoilers
revealSpoilers();
// Set up MutationObserver to watch for new spoilers
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.addedNodes.length > 0) {
// Check if new nodes contain spoilers
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
const newSpoilers = node.querySelectorAll('span.spoiler');
newSpoilers.forEach(spoiler => {
spoiler.style.background = 'none';
spoiler.style.color = 'inherit';
spoiler.style.textShadow = 'none';
});
}
});
}
});
});
// Observe the document body for changes (new posts)
observer.observe(document.body, {
childList: true,
subtree: true
});
})();
//Hash navigation
// Add # links to backlinks and quote links for scrolling
(function() {
// Function to add # link to backlinks and quote links
function addHashLinks(container = document) {
const links = container.querySelectorAll('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink');
links.forEach(link => {
// Skip if # link already exists or processed
if (link.nextSibling && link.nextSibling.classList && link.nextSibling.classList.contains('hash-link-container')) return;
if (link.dataset.hashProcessed) return;
// Create # link as a span to avoid <a> processing
const hashLink = document.createElement('span');
hashLink.textContent = ' #';
hashLink.style.cursor = 'pointer';
hashLink.style.color = '#0000EE'; // Match link color
hashLink.title = 'Scroll to post';
hashLink.className = 'hash-link';
hashLink.dataset.hashListener = 'true'; // Mark as processed
// Wrap # link in a span to isolate it
const container = document.createElement('span');
container.className = 'hash-link-container';
container.appendChild(hashLink);
link.insertAdjacentElement('afterend', container);
link.dataset.hashProcessed = 'true'; // Mark as processed
});
}
// Event delegation for hash link clicks to mimic .linkSelf behavior
document.addEventListener('click', function(e) {
if (e.target.classList.contains('hash-link')) {
e.preventDefault();
e.stopPropagation();
const link = e.target.closest('.hash-link-container').previousElementSibling;
const postId = link.textContent.replace('>>', '');
if (document.getElementById(postId)) {
window.location.hash = `#${postId}`;
console.log(`Navigated to post #${postId}`);
} else {
console.log(`Post ${postId} not found`);
}
}
}, true);
// Process existing backlinks and quote links on page load
addHashLinks();
console.log('Hash links applied on page load');
// Patch inline reply logic to apply hash links to new inline content
if (window.tooltips) {
// Patch loadTooltip to apply hash links after content is loaded
const originalLoadTooltip = tooltips.loadTooltip;
tooltips.loadTooltip = function(element, quoteUrl, sourceId, isInline) {
originalLoadTooltip.apply(this, arguments);
if (isInline) {
// Wait for content to be fully loaded
setTimeout(() => {
addHashLinks(element);
console.log('Hash links applied to loaded tooltip content:', quoteUrl);
}, 0);
}
};
// Patch addLoadedTooltip to ensure hash links are applied
const originalAddLoadedTooltip = tooltips.addLoadedTooltip;
tooltips.addLoadedTooltip = function(htmlContents, tooltip, quoteUrl, replyId, isInline) {
originalAddLoadedTooltip.apply(this, arguments);
if (isInline) {
addHashLinks(htmlContents);
console.log('Hash links applied to inline tooltip content:', quoteUrl);
}
};
// Patch addInlineClick to apply hash links after appending
const originalAddInlineClick = tooltips.addInlineClick;
tooltips.addInlineClick = function(quote, innerPost, isBacklink, quoteTarget, sourceId) {
if (!quote.href || quote.classList.contains('hash-link') || quote.closest('.hash-link-container') || quote.href.includes('#q')) {
console.log('Skipped invalid or hash link:', quote.href || quote.textContent);
return;
}
// Clone quote to remove existing listeners
const newQuote = quote.cloneNode(true);
quote.parentNode.replaceChild(newQuote, quote);
quote = newQuote;
// Reapply hover events
tooltips.addHoverEvents(quote, innerPost, quoteTarget, sourceId);
console.log('Hover events reapplied for:', quoteTarget.quoteUrl);
// Add click handler
quote.addEventListener('click', function(e) {
console.log('linkQuote clicked:', quoteTarget.quoteUrl);
if (!tooltips.inlineReplies) {
console.log('inlineReplies disabled');
return;
}
e.preventDefault();
e.stopPropagation();
// Find or create replyPreview
let replyPreview = innerPost.querySelector('.replyPreview');
if (!replyPreview) {
replyPreview = document.createElement('div');
replyPreview.className = 'replyPreview';
innerPost.appendChild(replyPreview);
}
// Check for duplicates or loading
if (tooltips.loadingPreviews[quoteTarget.quoteUrl] ||
tooltips.quoteAlreadyAdded(quoteTarget.quoteUrl, innerPost)) {
console.log('Duplicate or loading:', quoteTarget.quoteUrl);
return;
}
// Create and load inline post
const placeHolder = document.createElement('div');
placeHolder.style.whiteSpace = 'normal';
placeHolder.className = 'inlineQuote';
tooltips.loadTooltip(placeHolder, quoteTarget.quoteUrl, sourceId, true);
// Verify post loaded
if (!placeHolder.querySelector('.linkSelf')) {
console.log('Failed to load post:', quoteTarget.quoteUrl);
return;
}
// Add close button
const close = document.createElement('a');
close.innerText = 'X';
close.className = 'closeInline';
close.onclick = () => placeHolder.remove();
placeHolder.querySelector('.postInfo').prepend(close);
// Process quotes in the new inline post
Array.from(placeHolder.querySelectorAll('.linkQuote'))
.forEach(a => tooltips.processQuote(a, false, true));
if (tooltips.bottomBacklinks) {
const alts = placeHolder.querySelector('.altBacklinks');
if (alts && alts.firstChild) {
Array.from(alts.firstChild.children)
.forEach(a => tooltips.processQuote(a, true));
}
}
// Append to replyPreview and apply hash links
replyPreview.appendChild(placeHolder);
addHashLinks(placeHolder);
console.log('Inline post appended and hash links applied:', quoteTarget.quoteUrl);
tooltips.removeIfExists();
}, true);
};
// Patch processQuote to skip hash links
const originalProcessQuote = tooltips.processQuote;
tooltips.processQuote = function(quote, isBacklink) {
if (!quote.href || quote.classList.contains('hash-link') || quote.closest('.hash-link-container') || quote.href.includes('#q')) {
console.log('Skipped invalid or hash link in processQuote:', quote.href || quote.textContent);
return;
}
originalProcessQuote.apply(this, arguments);
};
}
// Set up MutationObserver to handle dynamically added or updated backlinks and quote links
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.addedNodes.length) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check for new backlink or quote link <a> elements
const newLinks = node.matches('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink') ? [node] : node.querySelectorAll('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink');
newLinks.forEach(link => {
addHashLinks(link.parentElement);
console.log('Hash links applied to new link:', link.textContent);
});
}
});
}
});
});
// Observe changes to the posts container
const postsContainer = document.querySelector('.divPosts') || document.body;
observer.observe(postsContainer, {
childList: true,
subtree: true
});
})();
//--Hash navigation
//Inline reply chains
(function() {
'use strict';
console.log('Userscript is running');
// Add CSS for visual nesting
const style = document.createElement('style');
style.innerHTML = `
.inlineQuote .replyPreview {
margin-left: 20px;
border-left: 1px solid #ccc;
padding-left: 10px;
}
.closeInline {
color: #ff0000;
cursor: pointer;
margin-left: 5px;
font-weight: bold;
}
`;
document.head.appendChild(style);
// Wait for tooltips to initialize
window.addEventListener('load', function() {
if (!window.tooltips) {
console.error('tooltips module not found');
return;
}
console.log('tooltips module found');
// Ensure Inline Replies is enabled
if (!tooltips.inlineReplies) {
console.log('Enabling Inline Replies');
localStorage.setItem('inlineReplies', 'true');
tooltips.inlineReplies = true;
// Check and update the checkbox, retrying if not yet loaded
const enableCheckbox = () => {
const inlineCheckbox = document.getElementById('settings-SW5saW5lIFJlcGxpZX');
if (inlineCheckbox) {
inlineCheckbox.checked = true;
console.log('Inline Replies checkbox checked');
return true;
}
console.warn('Inline Replies checkbox not found, retrying...');
return false;
};
// Try immediately
if (!enableCheckbox()) {
// Retry every 500ms up to 5 seconds
let attempts = 0;
const maxAttempts = 10;
const interval = setInterval(() => {
if (enableCheckbox() || attempts >= maxAttempts) {
clearInterval(interval);
if (attempts >= maxAttempts) {
console.error('Failed to find Inline Replies checkbox after retries');
}
}
attempts++;
}, 500);
}
} else {
console.log('Inline Replies already enabled');
}
// Override addLoadedTooltip to ensure replyPreview exists
const originalAddLoadedTooltip = tooltips.addLoadedTooltip;
tooltips.addLoadedTooltip = function(htmlContents, tooltip, quoteUrl, replyId, isInline) {
console.log('addLoadedTooltip called for:', quoteUrl);
originalAddLoadedTooltip.apply(this, arguments);
if (isInline) {
let replyPreview = htmlContents.querySelector('.replyPreview');
if (!replyPreview) {
replyPreview = document.createElement('div');
replyPreview.className = 'replyPreview';
htmlContents.appendChild(replyPreview);
}
}
};
// Override addInlineClick for nested replies, excluding post number links
tooltips.addInlineClick = function(quote, innerPost, isBacklink, quoteTarget, sourceId) {
// Skip post number links (href starts with #q)
if (quote.href.includes('#q')) {
console.log('Skipping post number link:', quote.href);
return;
}
// Remove existing listeners by cloning
const newQuote = quote.cloneNode(true);
quote.parentNode.replaceChild(newQuote, quote);
quote = newQuote;
// Reapply hover events to preserve preview functionality
tooltips.addHoverEvents(quote, innerPost, quoteTarget, sourceId);
console.log('Hover events reapplied for:', quoteTarget.quoteUrl);
// Add click handler
quote.addEventListener('click', function(e) {
console.log('linkQuote clicked:', quoteTarget.quoteUrl);
if (!tooltips.inlineReplies) {
console.log('inlineReplies disabled');
return;
}
e.preventDefault();
e.stopPropagation(); // Prevent site handlers
// Find or create replyPreview
let replyPreview = innerPost.querySelector('.replyPreview');
if (!replyPreview) {
replyPreview = document.createElement('div');
replyPreview.className = 'replyPreview';
innerPost.appendChild(replyPreview);
}
// Check for duplicates or loading
if (tooltips.loadingPreviews[quoteTarget.quoteUrl] ||
tooltips.quoteAlreadyAdded(quoteTarget.quoteUrl, innerPost)) {
console.log('Duplicate or loading:', quoteTarget.quoteUrl);
return;
}
// Create and load inline post
const placeHolder = document.createElement('div');
placeHolder.style.whiteSpace = 'normal';
placeHolder.className = 'inlineQuote';
tooltips.loadTooltip(placeHolder, quoteTarget.quoteUrl, sourceId, true);
// Verify post loaded
if (!placeHolder.querySelector('.linkSelf')) {
console.log('Failed to load post:', quoteTarget.quoteUrl);
return;
}
// Add close button
const close = document.createElement('a');
close.innerText = 'X';
close.className = 'closeInline';
close.onclick = () => placeHolder.remove();
placeHolder.querySelector('.postInfo').prepend(close);
// Process quotes in the new inline post
Array.from(placeHolder.querySelectorAll('.linkQuote'))
.forEach(a => tooltips.processQuote(a, false, true));
if (tooltips.bottomBacklinks) {
const alts = placeHolder.querySelector('.altBacklinks');
if (alts && alts.firstChild) {
Array.from(alts.firstChild.children)
.forEach(a => tooltips.processQuote(a, true));
}
}
// Append to replyPreview
replyPreview.appendChild(placeHolder);
console.log('Inline post appended:', quoteTarget.quoteUrl);
tooltips.removeIfExists();
}, true); // Use capture phase
};
// Reprocess all existing linkQuote and backlink elements, excluding post numbers
console.log('Reprocessing linkQuote elements');
const quotes = document.querySelectorAll('.linkQuote, .panelBacklinks a');
quotes.forEach(quote => {
const innerPost = quote.closest('.innerPost, .innerOP');
if (!innerPost) {
console.log('No innerPost found for quote:', quote.href);
return;
}
// Skip post number links
if (quote.href.includes('#q')) {
console.log('Skipping post number link:', quote.href);
return;
}
const isBacklink = quote.parentElement.classList.contains('panelBacklinks') ||
quote.parentElement.classList.contains('altBacklinks');
const quoteTarget = api.parsePostLink(quote.href);
const sourceId = api.parsePostLink(innerPost.querySelector('.linkSelf').href).post;
tooltips.addInlineClick(quote, innerPost, isBacklink, quoteTarget, sourceId);
});
// Observe for dynamically added posts
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType !== 1) return;
const newQuotes = node.querySelectorAll('.linkQuote, .panelBacklinks a');
newQuotes.forEach(quote => {
if (quote.dataset.processed || quote.href.includes('#q')) {
if (quote.href.includes('#q')) {
console.log('Skipping post number link:', quote.href);
}
return;
}
quote.dataset.processed = 'true';
const innerPost = quote.closest('.innerPost, .innerOP');
if (!innerPost) return;
const isBacklink = quote.parentElement.classList.contains('panelBacklinks') ||
quote.parentElement.classList.contains('altBacklinks');
const quoteTarget = api.parsePostLink(quote.href);
const sourceId = api.parsePostLink(innerPost.querySelector('.linkSelf').href).post;
tooltips.addInlineClick(quote, innerPost, isBacklink, quoteTarget, sourceId);
});
});
});
});
observer.observe(document.querySelector('.divPosts') || document.body, {
childList: true,
subtree: true
});
console.log('MutationObserver set up');
});
})();
//--Inline replies
//Auto TOS accept
(function() {
'use strict';
// Check if on the disclaimer page
if (window.location.pathname === '/.static/pages/disclaimer.html') {
// Redirect to confirmed page
window.location.replace('https://8chan.se/.static/pages/confirmed.html');
console.log('Automatically redirected from disclaimer to confirmed page');
}
})();
//--Auto TOS accept