There's another anon adding various QoL features like (You) notification and nested quote posts here, you should check it out. Here's the thread he's operating in if you want to make requests as I'll probably not add anything new here.

Don't copy <style>, </style>, <script>, and </script>, they're just there to look pretty.

This script is meant to:
>Highlight (You)r posts and posts that reply to (You)
>Append (You) to your name and posts you reply to for easier searching
>Unspoiler image thumbnails (thanks to this Anon for the improvement)
>Add a button next to Refresh to allow you to sort posts by ID (credits to this Anon's chatgpt'd script, I modified it to make the process reversible)
>Give an optional way to manually add IDs as (You)
>Pin the navigation/status bar at the bottom and add some more buttons to it (credits to this Anon for the idea)
>Append youtube titles to their links
>Beeps when someone quotes (You)
>Hide "[Unhide post /vyt/_____]" and "[Manage Board] [Moderate Board] [Moderate Thread]"

Steps:

  1. Click the gear icon, which should show this
  2. Click on CSS and copy the CSS section below (don't copy <style> and </style>), then paste it into the CSS field and press Save
  3. Do the same for JS (remember to not copy <script> and </script> and to press Save)
  4. Refresh the page and check CSS and JS in your Settings again to doublecheck, and then you should be good.

CSS

<style>
/* Gives posts that reply to (You) a solid border */
div.innerPost:has(a.you) {
    border: 2px solid #ff0000 !important;
    background-color: rgba(255, 0, 0, 0.05) !important;
}

/* Gives (You)r posts a dashed border (change border to border-left for 4chanX style) */
div.innerPost:has(a.youName) {
    border: 2px dashed #ff8080 !important;
    background-color: rgba(255, 102, 102, 0.05) !important;
}

/* Makes Posts/UIDS/Files centered and pinned to the bottom */
.threadBottom {
    display: flex;
    justify-content: space-between;
    align-items: center;
    position: fixed;
    bottom: 0;
    z-index: 1;
    width: 100%;
    left: 0;
    background: var(--background-color);
    padding: 5px 0;
}

/* Makes [Index] -> [Refresh] centered*/
.innerUtility {
    margin-top: 0;
    font-size: 100%;
}

/* Hover */
.quoteTooltip {
    background-color: var(--background-color);
}

/* Hides "[Unhide post /vyt/_____]", remove this section if you want to keep that */
.unhideButton.glowOnHover {
  display: none !important;
}

/* Removes excess & unselectable (You) on your name */
a.linkName.noEmailName.youName::after {
  display: none;
}

/* Removes excess & unselectable (You) on quotes */
a.quoteLink.you::after {
  display: none;
}

/* Removes "[Manage Board] [Moderate Board] [Moderate Thread]" */
#boardContentLinks {
  display: none !important;
}
</style>

JS

<script>
const manualIds = ['aaaaaa', 'bbbbbb']; // Modify 'aaaaaa' and 'bbbbbb' of your choice to mark them as (You)
const enableQuoteBeep = false; // Set to true to enable beep sound when someone quotes you

const enableUnspoiler = true; // Set to false to disable unspoiling image thumbnails
const enableAnonymousYou = true; // Set to false to disable Anonymous (You) names
const enableQuotelinkYou = true; // Set to false to disable (You) in quote links
const enableBacklinkYou = true; // Set to false to disable (You) in backlinks | Buggy atm, not sure how to fix
const enableYouTubeTitle = true; // Set to false to disable YouTube title fetching

//For the newly pinned navbar from the CSS section
const enableBottomButton = true;
const enableRefreshButton = true;
const enableRemoveIndexButton = false;
const enableRemoveCatalogButton = false;
const enableRemoveArchiveButton = false;

let originalPostCells = [];
let isSorted = false;
let prevLastReplyId = 0;
const titleCache = new Map();

document.addEventListener('DOMContentLoaded', () => {
    addSortButton(); // Adds JUDGE button next to Refresh
    addBottomButton(); // Adds [Bottom] in the bottom bar
    addRefreshButton(); // Adds [Refresh] in the bottom bar
    removeIndexButton(); // Remove [Index] button in the bottom bar
    removeCatalogButton(); // Remove [Catalog] button in the bottom bar
    removeArchiveButton(); // Remove [Archive] button in the bottom bar
    processPosts();
    initQuoteBeepObserver();

    const observer = new MutationObserver((mutations) => {
        let needsProcessing = false;
        mutations.forEach((mutation) => {
            if (mutation.addedNodes.length) {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeType === 1) {
                        if (node.matches('.quoteTooltip')) {
                            processQuoteTooltip(node, myPostIds);
                        } else if (
                            node.matches('.postCell') ||
                            node.matches('.innerPost') ||
                            node.querySelector('.postCell, .innerPost')
                        ) {
                            needsProcessing = true;
                        }
                    }
                });
            }
        });
        if (needsProcessing) {
            processPosts();
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
});

let myPostIds = [];

async function processPosts() {
    const manualPostIds = applyManualYouNames(manualIds);

    const myPosts = document.querySelectorAll('.postCell .postInfo .linkName.youName');
    myPostIds = Array.from(myPosts).map(post => post.closest('.postCell')?.id).filter(Boolean);

    const allMyPostIds = [...new Set([...manualPostIds, ...myPostIds])];

    document.querySelectorAll('.postCell, .innerPost').forEach(element => {
        processElement(element, allMyPostIds);
    });
}

function processQuoteTooltip(tooltip, postIds) {
    const quotelinks = tooltip.querySelectorAll('.divMessage a.quoteLink');
    quotelinks.forEach(link => {
        const match = link.getAttribute('href')?.match(/#q?(\d+)/);
        if (match) {
            const linkPostId = match[1];
            if (postIds.includes(linkPostId)) {
                if (!link.classList.contains('you')) {
                    link.classList.add('you');
                    link.textContent = `>>${linkPostId} (You)`;
                }
            }
        }
    });
}

async function processElement(element, myPostIds) {
    // Unspoiler image thumbnails
    if (enableUnspoiler) {
        var arr = document.querySelectorAll("img[src='/spoiler.png']");
        arr.forEach(element => {
            const parentLink = element.parentElement;
            if (parentLink && parentLink.href && parentLink.href.includes('/.media/')) {
                const filename = parentLink.href.split('/.media/')[1].split('.')[0];
                element.src = `/.media/t_${filename}`;
                element.style.border = "2px solid purple";
                element.style.boxShadow = "0 0 5px purple";
            }
        });
    }

    // Append (You)s for ctrl+f
    // To your name
    if (enableAnonymousYou) {
        const linkName = element.querySelector('.postInfo .linkName.youName');
        if (linkName) {
            linkName.textContent = 'Anonymous (You)';
        }
    }

    // To quote links
    if (enableQuotelinkYou) {
        const quotelinks = element.querySelectorAll('a.quoteLink');
        quotelinks.forEach(link => {
            const match = link.getAttribute('href')?.match(/#q?(\d+)/);
            if (match) {
                const linkPostId = match[1];
                if (myPostIds.includes(linkPostId)) {
                    if (!link.classList.contains('you')) {
                        link.classList.add('you');
                    }
                    link.textContent = `>>${linkPostId} (You)`;
                }
            }
        });
    }

    // To back links
    if (enableBacklinkYou) {
        const backlinks = element.querySelectorAll('.panelBacklinks a');
        backlinks.forEach(link => {
            const match = link.getAttribute('href')?.match(/#q?(\d+)/);
            if (match) {
                const linkPostId = match[1];
                if (myPostIds.includes(linkPostId)) {
                    link.textContent = `>>${linkPostId} (You)`;
                }
            }
        });
    }

    //Process YouTube links
    if (enableYouTubeTitle) {
        await processYouTubeLinks(element);
    }
}

function sortPostCells() {
    const parentContainer = document.querySelector('.postCell').parentElement;

    if (!isSorted) {
        originalPostCells = Array.from(document.querySelectorAll('.postCell'));

        const sortedCells = [...originalPostCells].sort((a, b) => {
            const aLabelId = a.querySelector('.labelId').textContent.trim();
            const bLabelId = b.querySelector('.labelId').textContent.trim();
            return aLabelId.localeCompare(bLabelId);
        });

        sortedCells.forEach(cell => parentContainer.appendChild(cell));
        isSorted = true;
        sortButton.value = 'Revert Judgement';
    } else {
        originalPostCells.forEach(cell => parentContainer.appendChild(cell));
        isSorted = false;
        sortButton.value = 'JUDGE';
    }
}

function addSortButton() {
    const refreshButton = document.getElementById('refreshButton');
    const sortButton = document.createElement('input');
    sortButton.type = 'button';
    sortButton.value = 'JUDGE';
    sortButton.id = 'sortButton';

    sortButton.addEventListener('click', sortPostCells);

    refreshButton.insertAdjacentElement('afterend', sortButton);
}

function addBottomButton() {
    if (!enableBottomButton) return;

    const navButtonContainer = document.querySelector('.threadBottom .innerUtility');
    const topButton = navButtonContainer?.querySelector('a.navButton[href="#"]');
    if (navButtonContainer && topButton) {
        const space = document.createTextNode(' ');
        const bottomButton = document.createElement('a');
        bottomButton.href = '#footer';
        bottomButton.textContent = 'Bottom';
        bottomButton.classList.add('navButton', 'bottomButton');

        topButton.insertAdjacentElement('afterend', bottomButton);
        topButton.insertAdjacentText('afterend', ' ');
    }
}

function addRefreshButton() {
    if (!enableRefreshButton) return;

    const navButtonContainer = document.querySelector('.threadBottom .innerUtility');
    if (navButtonContainer) {
        const refreshButton = document.createElement('a');
        refreshButton.href = 'javascript:void(0);';
        refreshButton.textContent = 'Refresh';
        refreshButton.classList.add('navButton', 'refreshButton');

        refreshButton.onclick = function() {
            thread.refreshPosts(true);
            thread.lastRefresh = 0;
        };

        navButtonContainer.appendChild(document.createTextNode(' '));
        navButtonContainer.appendChild(refreshButton);
    }
}

function removeIndexButton() {
    if (!enableRemoveIndexButton) return;

    const navButtonContainer = document.querySelector('.threadBottom .innerUtility');
    if (!navButtonContainer) return;

    const indexButton = Array.from(navButtonContainer.querySelectorAll('a'))
        .find(a => a.textContent.trim() === 'Index' && a.getAttribute('href') === '../');

    if (indexButton) indexButton.remove();
}

function removeCatalogButton() {
    if (!enableRemoveCatalogButton) return;

    const navButtonContainer = document.querySelector('.threadBottom .innerUtility');
    if (!navButtonContainer) return;

    const catalogButton = Array.from(navButtonContainer.querySelectorAll('a'))
        .find(a => a.textContent.trim() === 'Catalog' && a.getAttribute('href') === '../catalog.html');

    if (catalogButton) catalogButton.remove();
}

function removeArchiveButton() {
    if (!enableRemoveArchiveButton) return;

    const navButtonContainer = document.querySelector('.threadBottom .innerUtility');
    if (!navButtonContainer) return;

    const archiveButton = navButtonContainer.querySelector('a.archiveLinkThread');
    if (archiveButton) archiveButton.remove();
}

const style = document.createElement('style');
style.textContent = `
  .youtube-preview {
    position: absolute;
    display: none;
    z-index: 1000;
    border: 1px solid #ccc;
    background: #fff;
    padding: 2px;
  }
`;
document.head.appendChild(style);

async function fetchYouTubeTitle(videoId) {
  if (titleCache.has(videoId)) {
    return titleCache.get(videoId);
  }

  try {
    const response = await fetch(
      `https://www.youtube.com/oembed?url=https%3A//www.youtube.com/watch%3Fv%3D${videoId}&format=json`
    );
    const data = await response.json();
    if (data.title) {
      titleCache.set(videoId, data.title);
      return data.title;
    }
    return null;
  } catch (error) {
    console.error('Error fetching YouTube title:', error);
    return null;
  }
}

async function processYouTubeLinks(element) {
    const messageDiv = element.querySelector('.divMessage');
    if (!messageDiv) return;

    const links = messageDiv.querySelectorAll('a:not(.quoteLink)');
    for (const link of links) {
      const href = link.getAttribute('href');
      if (!href) continue;

      const youtubeRegex = /(?:youtube\.com\/(?:.*[?&]v=|(?:v|embed|shorts|live)\/)|youtu\.be\/)([\w\-]{11})/;
      const match = href.match(youtubeRegex);
      if (match) {
        const videoId = match[1];
        if (!link.classList.contains('youtube-processed')) {
          // Append title
          const title = await fetchYouTubeTitle(videoId);
          if (title) {
            link.textContent = `${href} | ${title}`;
            link.classList.add('youtube-processed');
          }
        }
      }
    }
}  

function applyManualYouNames(manualIds) {
    const manualPostIds = [];

    document.querySelectorAll('.postCell, .innerPost').forEach(container => {
        const idSpan = container.querySelector('.spanId .labelId');
        const idText = idSpan?.textContent?.trim();

        if (idText && manualIds.includes(idText)) {
            const linkName = container.querySelector('.postInfo .linkName');
            if (linkName && !linkName.classList.contains('youName')) {
                linkName.classList.add('youName');
                linkName.textContent = 'Anonymous (You)';
            }

            const postId =
                container.closest('.postCell')?.id ||
                container.querySelector('.linkQuote, .linkSelf')?.getAttribute('href')?.match(/#q?(\d+)/)?.[1];

            if (postId) {
                manualPostIds.push(postId);
            }
        }
    });

    document.querySelectorAll('a.quoteLink').forEach(link => {
        const href = link.getAttribute('href');
        const match = href?.match(/#q?(\d+)/);
        if (match) {
            const quotedId = match[1];
            if (manualPostIds.includes(quotedId)) {
                if (!link.classList.contains('you')) {
                    link.classList.add('you');
                    if (!link.textContent.includes('(You)')) {
                        link.textContent += ' (You)';
                    }
                }
            }
        }
    });

    return manualPostIds;
}

function initQuoteBeepObserver() {
    if (!enableQuoteBeep) return;

    const beep = new Audio('data:audio/wav;base64,UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA');

    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === 1 && node.querySelector('a.quoteLink.you')) {
                    console.log('[YouQuoteBeep] You were quoted.');
                    playBeep();
                }
            });
        });
    });

    observer.observe(document.body, { childList: true, subtree: true });

    function playBeep() {
        if (beep.paused) {
            beep.play().catch(e => console.warn("[YouQuoteBeep] Beep failed:", e));
        } else {
            beep.addEventListener('ended', () => beep.play(), { once: true });
        }
    }
}
</script>

If you're annoyed that some of your posts with the same ID don't work, there's a bandaid solution included. Modify the ids in const manualIds = ['aaaaaa', 'bbbbbb']; to your choice to whatever IDs you want to identify as (You).

Edit Report
Pub: 16 Apr 2025 11:36 UTC
Edit: 20 Apr 2025 10:05 UTC
Views: 373