• Hey Guest,

    We wanted to share a quick update with the community.

    Our public expense ledger is now live, allowing anyone to see how donations are used to support the ongoing operation of the site.

    👉 View the ledger here

    Over the past year, increased regulatory pressure in multiple regions like UK OFCOM and Australia's eSafety has led to higher operational costs, including infrastructure, security, and the need to work with more specialized service providers to keep the site online and stable.

    If you value the community and would like to help support its continued operation, donations are greatly appreciated. If you wish to donate via Bank Transfer or other options, please open a ticket.

    Donate via cryptocurrency:

    Bitcoin (BTC):
    Ethereum (ETH):
    Monero (XMR):
heywey

heywey

Student
Aug 28, 2025
130
There have been a few threads in the suggestions subforum that can be done client-side, plus some other things that annoyed me about the site ui, so I made a little userscript to fix them. It can:
  • hide those useless chat notification bubbles
  • hide the word "suicide" in headers and page titles
  • set maximum signature height
  • prevent gifs in signatures from autoplaying
  • automatically set dark/light mode based on browser/system preference
To use it you'll need a browser extension like Tampermonkey (Chrom[e|ium]) or Violentmonkey (Firefox), then add a new script and copy-paste this in. I'd just upload it to greasyfork but don't really want my account there associated with this site so here we are.

Also this should go without saying but running code that you don't understand from some random guy you don't know is generally a pretty bad idea. It's simple and tiny so it should be pretty easy to follow if you have any programming knowledge, but if you don't, idk, ask AI to walk you through it or something.

JavaScript:
// ==UserScript==
// @name       sasu aio
// @namespace  Violentmonkey Scripts
// @match      https://sanctioned-suicide.net/*
// @grant      none
// @version    3.1
// @author     heywey
// @description
// ==/UserScript==

(function() {
    'use strict';

    //// SETTINGS
    // privacy
    const REPLACEMENT_WORD = "suicide"; // 'suicide' in headers (forum names, etc) gets replaced w this. just set to 'suicide' to disable
    const REMOVE_SITE_NAME = false; // removes " | Sanctioned Suicide" from browser tab title
    // signatures
    const MAX_SIGNATURE_HEIGHT = 150; // max signature height in pixels (overflow gets a scrollbar)
    const PREVENT_GIF_AUTOPLAY = true; // replaces gifs in signatures with black box until hovered with cursor
    // visual tweaks
    const MATCH_BROWSER_THEME = true; // automatically sets site theme to match browser preference (dark/light)
    const HIDE_BADGE_INDICATOR = true; // hides chat notification bubble

    //// UTIL

    function injectCss(css) {
        if (!css) return;
        const style = document.createElement('style');
        style.textContent = css;
        document.head.appendChild(style);
    }

    function capitalize(str) {
        return str.charAt(0).toUpperCase() + str.slice(1);
    }

    //// MODULES

    function applyReplacementWord() {
        if (REPLACEMENT_WORD.toLowerCase() === 'suicide') return;

        const replacerLower = REPLACEMENT_WORD.toLowerCase();
        const replacerTitle = capitalize(replacerLower);

        // helper to replace text in nodes
        function replaceTextInNode(node) {
            // skip script, style, form elements
            if (['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT'].includes(node.tagName)) return;

            if (node.nodeType === Node.TEXT_NODE) {
                const originalText = node.textContent;
                const newText = originalText
                    .replace(/Suicide/g, replacerTitle)
                    .replace(/suicide/g, replacerLower);

                if (newText !== originalText) {
                    node.textContent = newText;
                }
                return;
            }

            if (node.nodeType === Node.ELEMENT_NODE && !node.isContentEditable) {
                for (let i = 0; i < node.childNodes.length; i++) {
                    replaceTextInNode(node.childNodes[i]);
                }
            }
        }

        // replace in headers
        const headers = document.querySelectorAll('h1, h2, h3, h4, h5');
        headers.forEach(header => replaceTextInNode(header));

        // replace in title
        let newTitle = document.title;
        newTitle = newTitle
            .replace(/Suicide/g, replacerTitle)
            .replace(/suicide/g, replacerLower);

        if (newTitle !== document.title) {
            document.title = newTitle;
        }
    }

    function applyRemoveSiteName() {
        if (!REMOVE_SITE_NAME) return;

        // removes " | Sanctioned Suicide" and any surrounding whitespace/pipes
        const newTitle = document.title.replace(/[\s|]*Sanctioned Suicide/gi, '');

        if (newTitle !== document.title) {
            document.title = newTitle;
        }
    }

    function applyMaxSignatureHeight() {
        if (MAX_SIGNATURE_HEIGHT <= 0) return;

        injectCss(`
            .message-signature {
                max-height: ${MAX_SIGNATURE_HEIGHT}px !important;
                overflow-y: auto !important;
                overflow-x: hidden !important;
            }
        `);
    }

    function applyPreventGifAutoplay() {
        if (!PREVENT_GIF_AUTOPLAY) return;

        // target images that look like gifs based on src, data-url, alt attributes
        const gifSelectors = [
            '.message-signature img[src*=".gif"]',
            '.message-signature img[data-url*=".gif"]',
            '.message-signature img[alt*=".gif"]'
        ].join(', ');

        const hoverSelectors = gifSelectors.split(', ').map(s => s + ':hover').join(', ');

        // set wrapper background to black and handle opacity
        injectCss(`
            .message-signature .bbImageWrapper {
                background-color: #000 !important;
                display: inline-block !important;
            }
            ${gifSelectors} {
                opacity: 0 !important;
            }
            /* show on hover */
            ${hoverSelectors} {
                opacity: 1 !important;
            }
        `);
    }

    function applyMatchBrowserTheme() {
        if (!MATCH_BROWSER_THEME) return;

        // check if browser supports media queries and site object exists
        if (window.matchMedia && typeof themehouse !== 'undefined' && themehouse.styleSwitch) {
            const isOsDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
            const targetMode = isOsDark ? 'dark' : 'light';

            // attempt to detect current site mode to avoid redundant switches
            let currentMode;

            // check internal property if available
            if (themehouse.styleSwitch.mode) {
                currentMode = themehouse.styleSwitch.mode;
            }
            // fallback: check html classes/attributes
            else {
                const html = document.documentElement;
                const isSiteDark = html.classList.contains('theme--dark') ||
                                   html.classList.contains('uix_dark') ||
                                   html.dataset.theme === 'dark';
                currentMode = isSiteDark ? 'dark' : 'light';
            }

            // only switch if the modes don't match
            if (currentMode !== targetMode) {
                themehouse.styleSwitch.switchStyle(targetMode);
            }
        }
    }

    function applyHideBadgeIndicator() {
        if (!HIDE_BADGE_INDICATOR) return;

        injectCss(`
            .badgeContainer::after {
                content: none !important;
            }
            .badge {
              display: none !important;
            }
        `);
    }

    //// INIT

    function init() {
        // privacy
        applyRemoveSiteName();
        applyReplacementWord();

        // signatures
        applyMaxSignatureHeight();
        applyPreventGifAutoplay();

        // visual tweaks
        applyMatchBrowserTheme();
        applyHideBadgeIndicator();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();
 
Last edited:
  • Like
  • Love
  • Informative
Reactions: avoid, OnceTheHappiestMan, martyrdom and 14 others
H

Hvergelmir

Warlock
May 5, 2024
773
Thanks for sharing this.

The script is safe. There's really no room for hidden malicious behavior.
 
  • Love
Reactions: heywey
heywey

heywey

Student
Aug 28, 2025
130
Added setting to show the "jump to new" at the top of every thread. Also fixed the overly broad notification bubble selector, whoops.

JavaScript:
// ==UserScript==
// @name       sasu aio
// @namespace  Violentmonkey Scripts
// @match      https://sanctioned-suicide.net/*
// @grant      none
// @version    4
// @author     heywey
// @description
// ==/UserScript==

(function() {
    'use strict';

    //// SETTINGS
    // privacy
    const REPLACEMENT_WORD = "suicide"; // 'suicide' in headers (forum names, etc) gets replaced w this. just set to 'suicide' to disable
    const REMOVE_SITE_NAME = false; // removes " | Sanctioned Suicide" from browser tab title
    // signatures
    const MAX_SIGNATURE_HEIGHT = 150; // max signature height in pixels (overflow gets a scrollbar)
    const PREVENT_GIF_AUTOPLAY = true; // replaces gifs in signatures with black box until hovered with cursor
    // visual tweaks
    const MATCH_BROWSER_THEME = true; // automatically sets site theme to match browser preference (dark/light)
    const HIDE_BADGE_INDICATOR = true; // hides chat notification bubble
    // navigation
    const ADD_JUMP_TO_NEW = true; // adds "Jump to new" button to threads where it is missing (links to latest post)

    //// UTIL

    function injectCss(css) {
        if (!css) return;
        const style = document.createElement('style');
        style.textContent = css;
        document.head.appendChild(style);
    }

    function capitalize(str) {
        return str.charAt(0).toUpperCase() + str.slice(1);
    }

    //// MODULES

    function applyReplacementWord() {
        if (REPLACEMENT_WORD.toLowerCase() === 'suicide') return;

        const replacerLower = REPLACEMENT_WORD.toLowerCase();
        const replacerTitle = capitalize(replacerLower);

        // helper to replace text in nodes
        function replaceTextInNode(node) {
            // skip script, style, form elements
            if (['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT'].includes(node.tagName)) return;

            if (node.nodeType === Node.TEXT_NODE) {
                const originalText = node.textContent;
                const newText = originalText
                    .replace(/Suicide/g, replacerTitle)
                    .replace(/suicide/g, replacerLower);

                if (newText !== originalText) {
                    node.textContent = newText;
                }
                return;
            }

            if (node.nodeType === Node.ELEMENT_NODE && !node.isContentEditable) {
                for (let i = 0; i < node.childNodes.length; i++) {
                    replaceTextInNode(node.childNodes[i]);
                }
            }
        }

        // replace in headers
        const headers = document.querySelectorAll('h1, h2, h3, h4, h5');
        headers.forEach(header => replaceTextInNode(header));

        // replace in title
        let newTitle = document.title;
        newTitle = newTitle
            .replace(/Suicide/g, replacerTitle)
            .replace(/suicide/g, replacerLower);

        if (newTitle !== document.title) {
            document.title = newTitle;
        }
    }

    function applyRemoveSiteName() {
        if (!REMOVE_SITE_NAME) return;

        // removes " | Sanctioned Suicide" and any surrounding whitespace/pipes
        const newTitle = document.title.replace(/[\s|]*Sanctioned Suicide/gi, '');

        if (newTitle !== document.title) {
            document.title = newTitle;
        }
    }

    function applyMaxSignatureHeight() {
        if (MAX_SIGNATURE_HEIGHT <= 0) return;

        injectCss(`
            .message-signature {
                max-height: ${MAX_SIGNATURE_HEIGHT}px !important;
                overflow-y: auto !important;
                overflow-x: hidden !important;
            }
        `);
    }

    function applyPreventGifAutoplay() {
        if (!PREVENT_GIF_AUTOPLAY) return;

        // target images that look like gifs based on src, data-url, alt attributes
        const gifSelectors = [
            '.message-signature img[src*=".gif"]',
            '.message-signature img[data-url*=".gif"]',
            '.message-signature img[alt*=".gif"]'
        ].join(', ');

        const hoverSelectors = gifSelectors.split(', ').map(s => s + ':hover').join(', ');

        // set wrapper background to black and handle opacity
        injectCss(`
            .message-signature .bbImageWrapper {
                background-color: #000 !important;
                display: inline-block !important;
            }
            ${gifSelectors} {
                opacity: 0 !important;
            }
            /* show on hover */
            ${hoverSelectors} {
                opacity: 1 !important;
            }
        `);
    }

    function applyMatchBrowserTheme() {
        if (!MATCH_BROWSER_THEME) return;

        // check if browser supports media queries and site object exists
        if (window.matchMedia && typeof themehouse !== 'undefined' && themehouse.styleSwitch) {
            const isOsDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
            const targetMode = isOsDark ? 'dark' : 'light';

            // attempt to detect current site mode to avoid redundant switches
            let currentMode;

            // check internal property if available
            if (themehouse.styleSwitch.mode) {
                currentMode = themehouse.styleSwitch.mode;
            }
            // fallback: check html classes/attributes
            else {
                const html = document.documentElement;
                const isSiteDark = html.classList.contains('theme--dark') ||
                                   html.classList.contains('uix_dark') ||
                                   html.dataset.theme === 'dark';
                currentMode = isSiteDark ? 'dark' : 'light';
            }

            // only switch if the modes don't match
            if (currentMode !== targetMode) {
                themehouse.styleSwitch.switchStyle(targetMode);
            }
        }
    }

    function applyHideBadgeIndicator() {
        if (!HIDE_BADGE_INDICATOR) return;

        injectCss(`
            a.p-navgroup-link--chat.badgeContainer.badgeContainer--highlighted::after {
                content: none !important;
            }
            .badge {
              display: none !important;
            }
        `);
    }

    function applyJumpToNew() {
        if (!ADD_JUMP_TO_NEW) return;

        // only run if we are in a thread
        if (!window.location.pathname.includes('/threads/')) return;

        // extract the canonical base url of the thread (e.g., /threads/name.123/)
        // we look for the pattern /threads/title.id/
        const match = window.location.pathname.match(/(\/threads\/[^/]+\.\d+\/)/);
        if (!match) return;

        const threadBaseUrl = match[1];
        // appending 'latest' to a xenforo thread url automatically redirects to the last post
        const targetUrl = threadBaseUrl + 'latest';

        // find all button groups in the outer blocks (usually one at top, one at bottom)
        const buttonGroups = document.querySelectorAll('.block-outer-opposite .buttonGroup');

        buttonGroups.forEach(group => {
            // check if a "jump to new" button already exists here
            // the native button usually has 'unread' in the href or the specific text
            const hasButton = Array.from(group.children).some(child =>
                child.textContent.trim() === 'Jump to new' ||
                (child.tagName === 'A' && child.href.includes('unread'))
            );

            if (!hasButton) {
                // create the button
                const btn = document.createElement('a');
                btn.className = 'button--link button rippleButton';
                btn.href = targetUrl;
                btn.innerHTML = '<span class="button-text">Jump to new</span>';

                // insert at the beginning of the group (standard position)
                group.insertBefore(btn, group.firstChild);
            }
        });
    }

    //// INIT

    function init() {
        // privacy
        applyRemoveSiteName();
        applyReplacementWord();

        // signatures
        applyMaxSignatureHeight();
        applyPreventGifAutoplay();

        // visual tweaks
        applyMatchBrowserTheme();
        applyHideBadgeIndicator();

        // navigation
        applyJumpToNew();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();
 
Last edited:
  • Informative
  • Like
Reactions: Surek and Forveleth
heywey

heywey

Student
Aug 28, 2025
130
Added a filter for 'similar threads' per https://sanctioned-suicide.net/thre...forum-games-to-only-other-forum-games.223621/

I can't rework the recommendation algorithm of course, so it just removes any posts that aren't from the current subforum.

Oh yeah, also removed the auto light/dark mode because it didn't work quite right and I can't figure out how to fix it.

JavaScript:
// ==UserScript==
// @name       sasu aio
// @namespace  Violentmonkey Scripts
// @match      https://sanctioned-suicide.net/*
// @grant      none
// @version    5
// @author     heywey
// @description
// ==/UserScript==

(function() {
    'use strict';

    //// SETTINGS
    // privacy
    const REPLACEMENT_WORD = "suicide"; // 'suicide' in headers (forum names, etc) gets replaced w this. just set to 'suicide' to disable
    const REMOVE_SITE_NAME = false; // removes " | Sanctioned Suicide" from browser tab title
    // signatures
    const MAX_SIGNATURE_HEIGHT = 150; // max signature height in pixels (overflow gets a scrollbar)
    const PREVENT_GIF_AUTOPLAY = true; // replaces gifs in signatures with black box until hovered with cursor
    // visual tweaks
    const HIDE_BADGE_INDICATOR = true; // hides chat notification bubble
    const HIDE_FOREIGN_SUGGESTIONS = true; // hides suggested threads that are not from the current subforum
    // navigation
    const ADD_JUMP_TO_NEW = true; // adds "Jump to new" button to threads where it is missing (links to latest post)

    //// UTIL

    function injectCss(css) {
        if (!css) return;
        const style = document.createElement('style');
        style.textContent = css;
        document.head.appendChild(style);
    }

    function capitalize(str) {
        return str.charAt(0).toUpperCase() + str.slice(1);
    }

    //// MODULES

    function applyReplacementWord() {
        if (REPLACEMENT_WORD.toLowerCase() === 'suicide') return;

        const replacerLower = REPLACEMENT_WORD.toLowerCase();
        const replacerTitle = capitalize(replacerLower);

        // helper to replace text in nodes
        function replaceTextInNode(node) {
            // skip script, style, form elements
            if (['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT'].includes(node.tagName)) return;

            if (node.nodeType === Node.TEXT_NODE) {
                const originalText = node.textContent;
                const newText = originalText
                    .replace(/Suicide/g, replacerTitle)
                    .replace(/suicide/g, replacerLower);

                if (newText !== originalText) {
                    node.textContent = newText;
                }
                return;
            }

            if (node.nodeType === Node.ELEMENT_NODE && !node.isContentEditable) {
                for (let i = 0; i < node.childNodes.length; i++) {
                    replaceTextInNode(node.childNodes[i]);
                }
            }
        }

        // replace in headers
        const headers = document.querySelectorAll('h1, h2, h3, h4, h5');
        headers.forEach(header => replaceTextInNode(header));

        // replace in title
        let newTitle = document.title;
        newTitle = newTitle
            .replace(/Suicide/g, replacerTitle)
            .replace(/suicide/g, replacerLower);

        if (newTitle !== document.title) {
            document.title = newTitle;
        }
    }

    function applyRemoveSiteName() {
        if (!REMOVE_SITE_NAME) return;

        // removes " | Sanctioned Suicide" and any surrounding whitespace/pipes
        const newTitle = document.title.replace(/[\s|]*Sanctioned Suicide/gi, '');

        if (newTitle !== document.title) {
            document.title = newTitle;
        }
    }

    function applyMaxSignatureHeight() {
        if (MAX_SIGNATURE_HEIGHT <= 0) return;

        injectCss(`
            .message-signature {
                max-height: ${MAX_SIGNATURE_HEIGHT}px !important;
                overflow-y: auto !important;
                overflow-x: hidden !important;
            }
        `);
    }

    function applyPreventGifAutoplay() {
        if (!PREVENT_GIF_AUTOPLAY) return;

        // target images that look like gifs based on src, data-url, alt attributes
        const gifSelectors = [
            '.message-signature img[src*=".gif"]',
            '.message-signature img[data-url*=".gif"]',
            '.message-signature img[alt*=".gif"]'
        ].join(', ');

        const hoverSelectors = gifSelectors.split(', ').map(s => s + ':hover').join(', ');

        // set wrapper background to black and handle opacity
        injectCss(`
            .message-signature .bbImageWrapper {
                background-color: #000 !important;
                display: inline-block !important;
            }
            ${gifSelectors} {
                opacity: 0 !important;
            }
            /* show on hover */
            ${hoverSelectors} {
                opacity: 1 !important;
            }
        `);
    }

    function applyHideBadgeIndicator() {
        if (!HIDE_BADGE_INDICATOR) return;

        injectCss(`
            a.p-navgroup-link--chat.badgeContainer.badgeContainer--highlighted::after {
                content: none !important;
            }
            .badge {
              display: none !important;
            }
        `);
    }

    function applyHideForeignSuggestions() {
        if (!HIDE_FOREIGN_SUGGESTIONS) return;

        // find similar threads widget
        const widget = document.querySelector('[data-widget-key="xfes_thread_view_below_quick_reply_similar_threads"]');
        if (!widget) return;

        // get current subforum by finding last link in the breadcrumb that points to a forum
        const breadcrumbLinks = Array.from(document.querySelectorAll('.p-breadcrumbs a'));
        const forumLinks = breadcrumbLinks.filter(a => a.href.includes('/forums/'));

        if (forumLinks.length === 0) return;

        // the last forum link in the chain is the current subforum
        const currentSubforumName = forumLinks[forumLinks.length - 1].textContent.trim();

        // iterate over suggested threads
        const threads = widget.querySelectorAll('.structItem--thread');

        threads.forEach(thread => {
            let threadSubforumName = null;

            // try to find the subforum link in the meta parts (standard location)
            const metaLinks = thread.querySelectorAll('.structItem-parts > li > a');
            for (const link of metaLinks) {
                // checks if href points to a forum
                if (link.href.includes('/forums/')) {
                    threadSubforumName = link.textContent.trim();
                    break;
                }
            }

            // if we found a subforum name and it doesn't match the current breadcrumb, remove it
            if (threadSubforumName && threadSubforumName !== currentSubforumName) {
                thread.remove();
            }
        });
    }

    function applyJumpToNew() {
        if (!ADD_JUMP_TO_NEW) return;

        // only run if we are in a thread
        if (!window.location.pathname.includes('/threads/')) return;

        // extract the canonical base url of the thread (e.g., /threads/name.123/)
        // we look for the pattern /threads/title.id/
        const match = window.location.pathname.match(/(\/threads\/[^/]+\.\d+\/)/);
        if (!match) return;

        const threadBaseUrl = match[1];
        // appending 'latest' to a xenforo thread url automatically redirects to the last post
        const targetUrl = threadBaseUrl + 'latest';

        // find all button groups in the outer blocks (usually one at top, one at bottom)
        const buttonGroups = document.querySelectorAll('.block-outer-opposite .buttonGroup');

        buttonGroups.forEach(group => {
            // check if a "jump to new" button already exists here
            // the native button usually has 'unread' in the href or the specific text
            const hasButton = Array.from(group.children).some(child =>
                child.textContent.trim() === 'Jump to new' ||
                (child.tagName === 'A' && child.href.includes('unread'))
            );

            if (!hasButton) {
                // create the button
                const btn = document.createElement('a');
                btn.className = 'button--link button rippleButton';
                btn.href = targetUrl;
                btn.innerHTML = '<span class="button-text">Jump to new</span>';

                // insert at the beginning of the group (standard position)
                group.insertBefore(btn, group.firstChild);
            }
        });
    }

    //// INIT

    function init() {
        // privacy
        applyRemoveSiteName();
        applyReplacementWord();

        // signatures
        applyMaxSignatureHeight();
        applyPreventGifAutoplay();

        // visual tweaks
        applyHideBadgeIndicator();
        applyHideForeignSuggestions();

        // navigation
        applyJumpToNew();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();
 
  • Wow
  • Like
Reactions: LoiteringClouds, NutOrat, Surek and 1 other person
U. A.

U. A.

"Ultra Based" gigashad
Aug 8, 2022
2,416
You are doing the admin's lord's work.
 
  • Yay!
  • Love
  • Like
Reactions: Dinorun, Xi-Xi, NutOrat and 1 other person
heywey

heywey

Student
Aug 28, 2025
130
Added normalize chat colors from https://sanctioned-suicide.net/threads/option-to-view-all-chat-messages-in-a-default-color.223753/
does what it says on the tin, chat message contents are always normal color

Also keyword filter as suggested in https://sanctioned-suicide.net/threads/keyword-list-that-auto-hides-content-containing-them.218025/

Feel free to @ with issues or whatever.

JavaScript:
// ==UserScript==
// @name       sasu aio
// @namespace  Violentmonkey Scripts
// @match      https://sanctioned-suicide.net/*
// @grant      none
// @version    7
// @author     heywey
// @description
// ==/UserScript==

(function() {
    'use strict';

    //// SETTINGS
    // privacy
    const REPLACEMENT_WORD = "suicide"; // 'suicide' in headers (forum names, etc) gets replaced w this. just set to 'suicide' to disable
    const REMOVE_SITE_NAME = false; // removes " | Sanctioned Suicide" from browser tab title
    // signatures
    const MAX_SIGNATURE_HEIGHT = 150; // max signature height in pixels (overflow gets a scrollbar)
    const PREVENT_GIF_AUTOPLAY = true; // replaces gifs in signatures with black box until hovered with cursor
    // visual tweaks
    const HIDE_BADGE_INDICATOR = true; // hides chat notification bubble
    const HIDE_FOREIGN_SUGGESTIONS = true; // hides suggested threads that are not from the current subforum
    const NORMALIZE_CHAT_COLOR = true; // removes cutom colors in chat. all message text becomes default color
    // navigation
    const ADD_JUMP_TO_NEW = true; // adds "Jump to new" button to threads where it is missing (links to latest post)
    // content filtering
    const FILTER_KEYWORDS = []; // example: ["keyword1", "keyword2"]
    const FILTER_THREADS = true; // remove threads containing keywords in title
    const FILTER_POSTS = true; // remove individual posts containing keywords

    //// UTIL

    function injectCss(css) {
        if (!css) return;
        const style = document.createElement('style');
        style.textContent = css;
        document.head.appendChild(style);
    }

    function capitalize(str) {
        return str.charAt(0).toUpperCase() + str.slice(1);
    }

    //// MODULES

    function applyReplacementWord() {
        if (REPLACEMENT_WORD.toLowerCase() === 'suicide') return;

        const replacerLower = REPLACEMENT_WORD.toLowerCase();
        const replacerTitle = capitalize(replacerLower);

        // helper to replace text in nodes
        function replaceTextInNode(node) {
            // skip script, style, form elements
            if (['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT'].includes(node.tagName)) return;

            if (node.nodeType === Node.TEXT_NODE) {
                const originalText = node.textContent;
                const newText = originalText
                    .replace(/Suicide/g, replacerTitle)
                    .replace(/suicide/g, replacerLower);

                if (newText !== originalText) {
                    node.textContent = newText;
                }
                return;
            }

            if (node.nodeType === Node.ELEMENT_NODE && !node.isContentEditable) {
                for (let i = 0; i < node.childNodes.length; i++) {
                    replaceTextInNode(node.childNodes[i]);
                }
            }
        }

        // replace in headers
        const headers = document.querySelectorAll('h1, h2, h3, h4, h5');
        headers.forEach(header => replaceTextInNode(header));

        // replace in title
        let newTitle = document.title;
        newTitle = newTitle
            .replace(/Suicide/g, replacerTitle)
            .replace(/suicide/g, replacerLower);

        if (newTitle !== document.title) {
            document.title = newTitle;
        }
    }

    function applyRemoveSiteName() {
        if (!REMOVE_SITE_NAME) return;

        // removes " | Sanctioned Suicide" and any surrounding whitespace/pipes
        const newTitle = document.title.replace(/[\s|]*Sanctioned Suicide/gi, '');

        if (newTitle !== document.title) {
            document.title = newTitle;
        }
    }

    function applyMaxSignatureHeight() {
        if (MAX_SIGNATURE_HEIGHT <= 0) return;

        injectCss(`
            .message-signature {
                max-height: ${MAX_SIGNATURE_HEIGHT}px !important;
                overflow-y: auto !important;
                overflow-x: hidden !important;
            }
        `);
    }

    function applyPreventGifAutoplay() {
        if (!PREVENT_GIF_AUTOPLAY) return;

        // target images that look like gifs based on src, data-url, alt attributes
        const gifSelectors = [
            '.message-signature img[src*=".gif"]',
            '.message-signature img[data-url*=".gif"]',
            '.message-signature img[alt*=".gif"]'
        ].join(', ');

        const hoverSelectors = gifSelectors.split(', ').map(s => s + ':hover').join(', ');

        // set wrapper background to black and handle opacity
        injectCss(`
            .message-signature .bbImageWrapper {
                background-color: #000 !important;
                display: inline-block !important;
            }
            ${gifSelectors} {
                opacity: 0 !important;
            }
            /* show on hover */
            ${hoverSelectors} {
                opacity: 1 !important;
            }
        `);
    }

    function applyHideBadgeIndicator() {
        if (!HIDE_BADGE_INDICATOR) return;

        injectCss(`
            a.p-navgroup-link--chat.badgeContainer.badgeContainer--highlighted::after {
                content: none !important;
            }
            .badge {
              display: none !important;
            }
        `);
    }

    function applyHideForeignSuggestions() {
        if (!HIDE_FOREIGN_SUGGESTIONS) return;

        // find similar threads widget
        const widget = document.querySelector('[data-widget-key="xfes_thread_view_below_quick_reply_similar_threads"]');
        if (!widget) return;

        // get current subforum by finding last link in the breadcrumb that points to a forum
        const breadcrumbLinks = Array.from(document.querySelectorAll('.p-breadcrumbs a'));
        const forumLinks = breadcrumbLinks.filter(a => a.href.includes('/forums/'));

        if (forumLinks.length === 0) return;

        // the last forum link in the chain is the current subforum
        const currentSubforumName = forumLinks[forumLinks.length - 1].textContent.trim();

        // iterate over suggested threads
        const threads = widget.querySelectorAll('.structItem--thread');

        threads.forEach(thread => {
            let threadSubforumName = null;

            // try to find the subforum link in the meta parts (standard location)
            const metaLinks = thread.querySelectorAll('.structItem-parts > li > a');
            for (const link of metaLinks) {
                // checks if href points to a forum
                if (link.href.includes('/forums/')) {
                    threadSubforumName = link.textContent.trim();
                    break;
                }
            }

            // if we found a subforum name and it doesn't match the current breadcrumb, remove it
            if (threadSubforumName && threadSubforumName !== currentSubforumName) {
                thread.remove();
            }
        });
    }

    function applyNormalizeChatColor() {
      if (!NORMALIZE_CHAT_COLOR) return;

      injectCss(`
          .siropuChatMessageText .fixed-color {
              color: inherit !important;
          }
      `);
    }

    function applyJumpToNew() {
        if (!ADD_JUMP_TO_NEW) return;

        // only run if we are in a thread
        if (!window.location.pathname.includes('/threads/')) return;

        // extract the canonical base url of the thread (e.g., /threads/name.123/)
        // we look for the pattern /threads/title.id/
        const match = window.location.pathname.match(/(\/threads\/[^/]+\.\d+\/)/);
        if (!match) return;

        const threadBaseUrl = match[1];
        // appending 'latest' to a xenforo thread url automatically redirects to the last post
        const targetUrl = threadBaseUrl + 'latest';

        // find all button groups in the outer blocks (usually one at top, one at bottom)
        const buttonGroups = document.querySelectorAll('.block-outer-opposite .buttonGroup');

        buttonGroups.forEach(group => {
            // check if a "jump to new" button already exists here
            // the native button usually has 'unread' in the href or the specific text
            const hasButton = Array.from(group.children).some(child =>
                child.textContent.trim() === 'Jump to new' ||
                (child.tagName === 'A' && child.href.includes('unread'))
            );

            if (!hasButton) {
                // create the button
                const btn = document.createElement('a');
                btn.className = 'button--link button rippleButton';
                btn.href = targetUrl;
                btn.innerHTML = '<span class="button-text">Jump to new</span>';

                // insert at the beginning of the group (standard position)
                group.insertBefore(btn, group.firstChild);
            }
        });
    }

    function applyKeywordFilter() {
        if (!FILTER_KEYWORDS.length) return;

        // create regex from keywords, escaping special chars
        const pattern = new RegExp(FILTER_KEYWORDS.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'i');

        if (FILTER_THREADS) {
            // standard thread list items
            const threads = document.querySelectorAll('.structItem--thread');
            threads.forEach(thread => {
                const title = thread.querySelector('.structItem-title');
                if (title && pattern.test(title.textContent)) {
                    thread.remove();
                }
            });

            // forum previews
            const extras = document.querySelectorAll('.node-extra');
            extras.forEach(extra => {
                const title = extra.querySelector('.node-extra-title');
                if (title && pattern.test(title.textContent)) {
                    extra.remove();
                }
            });
        }

        if (FILTER_POSTS) {
            const posts = document.querySelectorAll('.message--post');
            posts.forEach(post => {
                const content = post.querySelector('.bbWrapper');
                if (content && pattern.test(content.textContent)) {
                    post.remove();
                }
            });
        }
    }

    //// INIT

    function init() {
        // privacy
        applyRemoveSiteName();
        applyReplacementWord();

        // signatures
        applyMaxSignatureHeight();
        applyPreventGifAutoplay();

        // visual tweaks
        applyHideBadgeIndicator();
        applyHideForeignSuggestions();
        applyNormalizeChatColor();

        // navigation
        applyJumpToNew();

        // filtering
        applyKeywordFilter();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();
 
Last edited:
  • Love
  • Informative
  • Like
Reactions: NutOrat, boddibo, Surek and 1 other person
U. A.

U. A.

"Ultra Based" gigashad
Aug 8, 2022
2,416
Can a mod pin this or something? If admin isn't gonna even give us the changes that would be fucking options then ffs the least they can do is help people who want to make them now and in the future.
 
  • Like
Reactions: NutOrat and nobodycaresaboutme
Pluto

Pluto

Cat Extremist
Dec 27, 2020
6,518
images
 
  • Love
  • Hugs
Reactions: NutOrat, heywey, Surek and 1 other person
cme-dme

cme-dme

wants to sleep forever
Feb 1, 2025
547
Holy crap you're incredible. This as well as the Load Chat History userscript are absolute must haves for this forum!
PS: You should add an @match for the site mirror sanctionedsuicide.site
 
Last edited:
  • Love
Reactions: heywey, NutOrat and U. A.
heywey

heywey

Student
Aug 28, 2025
130
Added 24h time option per https://sanctioned-suicide.net/threads/option-for-normal-time-not-only-am-pm.230281/
(and added the mirror, thanks cme-dme)

JavaScript:
// ==UserScript==
// @name       sasu aio
// @namespace  Violentmonkey Scripts
// @match      https://sanctioned-suicide.net/*
// @match      https://sanctionedsuicide.site/*
// @grant      none
// @version    8
// @author     heywey
// @description
// ==/UserScript==

(function() {
    'use strict';

    //// SETTINGS
    // privacy
    const REPLACEMENT_WORD = "suicide"; // 'suicide' in headers (forum names, etc) gets replaced w this. just set to 'suicide' to disable
    const REMOVE_SITE_NAME = false; // removes " | Sanctioned Suicide" from browser tab title
    // signatures
    const MAX_SIGNATURE_HEIGHT = 150; // max signature height in pixels (overflow gets a scrollbar)
    const PREVENT_GIF_AUTOPLAY = true; // replaces gifs in signatures with black box until hovered with cursor
    // visual tweaks
    const HIDE_BADGE_INDICATOR = true; // hides chat notification bubble
    const HIDE_FOREIGN_SUGGESTIONS = true; // hides suggested threads that are not from the current subforum
    const NORMALIZE_CHAT_COLOR = true; // removes cutom colors in chat. all message text becomes default color
    const USE_24H_TIME = false; // converts AM/PM to 24h time
    // navigation
    const ADD_JUMP_TO_NEW = true; // adds "Jump to new" button to threads where it is missing (links to latest post)
    // content filtering
    const FILTER_KEYWORDS = []; // example: ["keyword1", "keyword2"]
    const FILTER_THREADS = true; // remove threads containing keywords in title
    const FILTER_POSTS = true; // remove individual posts containing keywords

    //// UTIL

    function injectCss(css) {
        if (!css) return;
        const style = document.createElement('style');
        style.textContent = css;
        document.head.appendChild(style);
    }

    function capitalize(str) {
        return str.charAt(0).toUpperCase() + str.slice(1);
    }

    //// MODULES

    function applyReplacementWord() {
        if (REPLACEMENT_WORD.toLowerCase() === 'suicide') return;

        const replacerLower = REPLACEMENT_WORD.toLowerCase();
        const replacerTitle = capitalize(replacerLower);

        function replaceTextInNode(node) {
            if (['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT'].includes(node.tagName)) return;

            if (node.nodeType === Node.TEXT_NODE) {
                const originalText = node.textContent;
                const newText = originalText
                    .replace(/Suicide/g, replacerTitle)
                    .replace(/suicide/g, replacerLower);

                if (newText !== originalText) {
                    node.textContent = newText;
                }
                return;
            }

            if (node.nodeType === Node.ELEMENT_NODE && !node.isContentEditable) {
                for (let i = 0; i < node.childNodes.length; i++) {
                    replaceTextInNode(node.childNodes[i]);
                }
            }
        }

        const headers = document.querySelectorAll('h1, h2, h3, h4, h5');
        headers.forEach(header => replaceTextInNode(header));

        let newTitle = document.title;
        newTitle = newTitle
            .replace(/Suicide/g, replacerTitle)
            .replace(/suicide/g, replacerLower);

        if (newTitle !== document.title) {
            document.title = newTitle;
        }
    }

    function applyRemoveSiteName() {
        if (!REMOVE_SITE_NAME) return;
        const newTitle = document.title.replace(/[\s|]*Sanctioned Suicide/gi, '');
        if (newTitle !== document.title) {
            document.title = newTitle;
        }
    }

    function applyMaxSignatureHeight() {
        if (MAX_SIGNATURE_HEIGHT <= 0) return;
        injectCss(`
            .message-signature {
                max-height: ${MAX_SIGNATURE_HEIGHT}px !important;
                overflow-y: auto !important;
                overflow-x: hidden !important;
            }
        `);
    }

    function applyPreventGifAutoplay() {
        if (!PREVENT_GIF_AUTOPLAY) return;
        const gifSelectors = [
            '.message-signature img[src*=".gif"]',
            '.message-signature img[data-url*=".gif"]',
            '.message-signature img[alt*=".gif"]'
        ].join(', ');
        const hoverSelectors = gifSelectors.split(', ').map(s => s + ':hover').join(', ');
        injectCss(`
            .message-signature .bbImageWrapper {
                background-color: #000 !important;
                display: inline-block !important;
            }
            ${gifSelectors} {
                opacity: 0 !important;
            }
            ${hoverSelectors} {
                opacity: 1 !important;
            }
        `);
    }

    function applyHideBadgeIndicator() {
        if (!HIDE_BADGE_INDICATOR) return;
        injectCss(`
            a.p-navgroup-link--chat.badgeContainer.badgeContainer--highlighted::after {
                content: none !important;
            }
            .badge {
              display: none !important;
            }
        `);
    }

    function applyHideForeignSuggestions() {
        if (!HIDE_FOREIGN_SUGGESTIONS) return;
        const widget = document.querySelector('[data-widget-key="xfes_thread_view_below_quick_reply_similar_threads"]');
        if (!widget) return;

        const breadcrumbLinks = Array.from(document.querySelectorAll('.p-breadcrumbs a'));
        const forumLinks = breadcrumbLinks.filter(a => a.href.includes('/forums/'));
        if (forumLinks.length === 0) return;

        const currentSubforumName = forumLinks[forumLinks.length - 1].textContent.trim();
        const threads = widget.querySelectorAll('.structItem--thread');

        threads.forEach(thread => {
            let threadSubforumName = null;
            const metaLinks = thread.querySelectorAll('.structItem-parts > li > a');
            for (const link of metaLinks) {
                if (link.href.includes('/forums/')) {
                    threadSubforumName = link.textContent.trim();
                    break;
                }
            }
            if (threadSubforumName && threadSubforumName !== currentSubforumName) {
                thread.remove();
            }
        });
    }

    function applyNormalizeChatColor() {
      if (!NORMALIZE_CHAT_COLOR) return;
      injectCss(`
          .siropuChatMessageText .fixed-color {
              color: inherit !important;
          }
      `);
    }

    // This function now accepts a 'root' element to scope the search
    // This makes it efficient when XF updates small parts of the page
    function apply24HourTime(root = document) {
        if (!USE_24H_TIME) return;

        // If root is a jQuery object (which XF uses), unwrap it
        const rootNode = root.jquery ? root[0] : root;

        const timeElements = rootNode.querySelectorAll('time.u-dt');

        timeElements.forEach(time => {
            const text = time.textContent;
            // Matches "8:22 AM", "8:22AM", "8:22   AM"
            // \s* allows for any amount of whitespace
            const match = text.match(/\b(\d{1,2}):(\d{2})\s*(AM|PM)\b/i);

            if (match) {
                let [fullMatch, hours, minutes, period] = match;
                hours = parseInt(hours, 10);
                period = period.toUpperCase();

                if (period === 'PM' && hours < 12) hours += 12;
                if (period === 'AM' && hours === 12) hours = 0;

                const hoursStr = hours.toString().padStart(2, '0');

                // Only replace if the text is actually different
                const newTimeStr = `${hoursStr}:${minutes}`;
                if (!fullMatch.includes(newTimeStr)) {
                     time.textContent = text.replace(fullMatch, newTimeStr);
                }
            }
        });
    }

    function hookXenForoTime() {
        if (!USE_24H_TIME) return;

        // Run immediately on whatever is currently loaded
        apply24HourTime();

        // Hook into XF.DynamicDate.refresh
        // This function is defined in the site JS and runs periodically or on interaction
        if (typeof window.XF !== 'undefined' && window.XF.DynamicDate) {
            const originalRefresh = window.XF.DynamicDate.refresh;

            // Overwrite the function
            window.XF.DynamicDate.refresh = function(root) {
                // Run the original site logic first (calculates "x minutes ago" etc)
                originalRefresh.apply(this, arguments);

                // Run our fix immediately after on the same elements
                apply24HourTime(root || document);
            };
        }
    }

    function applyJumpToNew() {
        if (!ADD_JUMP_TO_NEW) return;
        if (!window.location.pathname.includes('/threads/')) return;

        const match = window.location.pathname.match(/(\/threads\/[^/]+\.\d+\/)/);
        if (!match) return;

        const threadBaseUrl = match[1];
        const targetUrl = threadBaseUrl + 'latest';
        const buttonGroups = document.querySelectorAll('.block-outer-opposite .buttonGroup');

        buttonGroups.forEach(group => {
            const hasButton = Array.from(group.children).some(child =>
                child.textContent.trim() === 'Jump to new' ||
                (child.tagName === 'A' && child.href.includes('unread'))
            );

            if (!hasButton) {
                const btn = document.createElement('a');
                btn.className = 'button--link button rippleButton';
                btn.href = targetUrl;
                btn.innerHTML = '<span class="button-text">Jump to new</span>';
                group.insertBefore(btn, group.firstChild);
            }
        });
    }

    function applyKeywordFilter() {
        if (!FILTER_KEYWORDS.length) return;
        const pattern = new RegExp(FILTER_KEYWORDS.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'i');

        if (FILTER_THREADS) {
            const threads = document.querySelectorAll('.structItem--thread');
            threads.forEach(thread => {
                const title = thread.querySelector('.structItem-title');
                if (title && pattern.test(title.textContent)) {
                    thread.remove();
                }
            });
            const extras = document.querySelectorAll('.node-extra');
            extras.forEach(extra => {
                const title = extra.querySelector('.node-extra-title');
                if (title && pattern.test(title.textContent)) {
                    extra.remove();
                }
            });
        }

        if (FILTER_POSTS) {
            const posts = document.querySelectorAll('.message--post');
            posts.forEach(post => {
                const content = post.querySelector('.bbWrapper');
                if (content && pattern.test(content.textContent)) {
                    post.remove();
                }
            });
        }
    }

    //// INIT

    function init() {
        // privacy
        applyRemoveSiteName();
        applyReplacementWord();

        // signatures
        applyMaxSignatureHeight();
        applyPreventGifAutoplay();

        // visual tweaks
        applyHideBadgeIndicator();
        applyHideForeignSuggestions();
        applyNormalizeChatColor();

        // Hooks
        hookXenForoTime();

        // navigation
        applyJumpToNew();

        // filtering
        applyKeywordFilter();
    }

    // Wait for window load to ensure XF object is fully populated,
    // although DOMContentLoaded is usually enough for the script tags to execute.
    if (document.readyState === 'complete') {
        init();
    } else {
        window.addEventListener('load', init);
    }
})();
 
  • Love
Reactions: NutOrat
U. A.

U. A.

"Ultra Based" gigashad
Aug 8, 2022
2,416
Hidden content
You need to reply to this thread or react to this post in order to see this content.
 
Last edited:
  • Like
  • Love
Reactions: boddibo, natori, gunmetalblue and 1 other person
heywey

heywey

Student
Aug 28, 2025
130
[Hidden content]
1. Yes, every update here is an update to the whole script. You copy-paste the new version into the extension, with the exception of any settings you want to keep at the top. Bit of a pain, I know. Seeing as this is pinned n all I'll work on making that a little easier with auto updates and persistent settings with a configuration menu on the site.

2. Generally, yes, these extensions do have horrifyingly broad permissions. If you're on Firefox, Violentmonkey is open source, with little cause for concern (though it wouldn't hurt to disable automatic extension updates to be safe). If you're on Chrome[-ium], unfortunately it looks like the only actively developed userscript manager is Tampermonkey which is closed source -- I wouldn't trust it, although Chrome and most Chromium flavors now support restricting extension permissions per-website, so you could give it only access to SaSu which is at least something.

There is a Chromium extension called Little Rat (open source) that lets you monitor and block network requests from other extensions. I would highly recommend this one if you're concerned about malicious extensions, it's a godsend honestly. Unless you're wanting automatic userscript updates there's really no reason a userscript manager needs to make any network requests, so you could disable Tampermonkey's without losing much.
 
Last edited:
  • Informative
Reactions: martyrdom and U. A.
M

martyrdom

Arcanist
Nov 3, 2025
427
If you're on Chrome[-ium], unfortunately it looks like the only actively developed userscript manager is Tampermonkey which is closed source
Violentmonkey works very well on several flavors of Chromium, ie. Brave.
 
  • Informative
Reactions: heywey and U. A.
U. A.

U. A.

"Ultra Based" gigashad
Aug 8, 2022
2,416
if you don't [have programming knowledge], ask AI to walk you through it
Hidden content
You need -1 more posts to view this content
 
  • Like
Reactions: heywey
heywey

heywey

Student
Aug 28, 2025
130
New version, largely rewritten. You'll want to just completely replace the previous version with this. Settings are now stored in a more robust way -- you can access them inside the standard SaSu preferences page, and they'll be saved across new versions without any manual editing necessary. Also, it's hosted on a proper userscript repository now, so you can automatically fetch updates if you want.

Other things were tweaked and fixed, and also added a report button per https://sanctioned-suicide.net/thre...wable-without-opening-an-entire-ticket.230961

Link: https://greasyfork.org/en/scripts/563657-sasu-improvements-script
JavaScript:
// ==UserScript==
// @name         SaSu Improvements Script
// @namespace    Violentmonkey Scripts
// @match        https://sanctioned-suicide.net/*
// @match        https://sanctionedsuicide.site/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        unsafeWindow
// @version      1.1.1
// @author       heywey
// @license      MIT
// @description  Fix some annoyances with Sanctioned Suicide forums.
// @downloadURL https://update.greasyfork.org/scripts/563657/SaSu%20Improvements%20Script.user.js
// @updateURL https://update.greasyfork.org/scripts/563657/SaSu%20Improvements%20Script.meta.js
// ==/UserScript==

(function() {
    'use strict';

    const DEFAULTS = {
        REPLACEMENT_WORD: "",
        REMOVE_SITE_NAME: false,
        MAX_SIGNATURE_HEIGHT: 150,
        PREVENT_GIF_AUTOPLAY: true,
        HIDE_BADGE_INDICATOR: true,
        NORMALIZE_CHAT_COLOR: true,
        USE_24H_TIME: false,
        ADD_JUMP_TO_NEW: true,
        ADD_REPORT_BUTTON: true,
        FILTER_KEYWORDS: [],
        HIDE_FOREIGN_SUGGESTIONS: true,
        FILTER_THREADS: true,
        FILTER_POSTS: true
    };

    const getSetting = (key) => GM_getValue(key, DEFAULTS[key]);

    const SETTINGS = {
        replacementWord: getSetting('REPLACEMENT_WORD'),
        removeSiteName: getSetting('REMOVE_SITE_NAME'),
        maxSigHeight: getSetting('MAX_SIGNATURE_HEIGHT'),
        preventGifAutoplay: getSetting('PREVENT_GIF_AUTOPLAY'),
        hideBadge: getSetting('HIDE_BADGE_INDICATOR'),
        normalizeChat: getSetting('NORMALIZE_CHAT_COLOR'),
        use24h: getSetting('USE_24H_TIME'),
        jumpToNew: getSetting('ADD_JUMP_TO_NEW'),
        reportButton: getSetting('ADD_REPORT_BUTTON'),
        keywords: getSetting('FILTER_KEYWORDS'),
        hideForeign: getSetting('HIDE_FOREIGN_SUGGESTIONS'),
        filterThreads: getSetting('FILTER_THREADS'),
        filterPosts: getSetting('FILTER_POSTS')
    };

    // Core utilities
    const injectCss = css => {
        if (!css) return;
        const style = document.createElement('style');
        style.textContent = css;
        document.head.appendChild(style);
    };

    const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1);

    // Settings menu injection
    const injectSettingsMenu = () => {
        if (!window.location.pathname.includes('/account/preferences')) return;

        const container = document.querySelector('.p-body-pageContent');
        const targetForm = document.querySelector('form[action="/account/preferences"]');
        if (!container || !targetForm) return;

        const keywordString = Array.isArray(SETTINGS.keywords) ? SETTINGS.keywords.join('\n') : '';

        const settingsBlock = document.createElement('div');
        settingsBlock.className = 'block';
        settingsBlock.style.marginBottom = '20px';

        // Add autocomplete="off" to all input fields
        settingsBlock.innerHTML = `
            <div class="block-container">
                <h2 class="block-header">SaSu Improvements Settings</h2>
                <div class="block-body">
                    <dl class="formRow">
                        <dt><div class="formRow-labelWrapper"><label class="formRow-label">Privacy</label></div></dt>
                        <dd>
                            <ul class="inputChoices" role="group">
                                <li class="inputChoices-choice" style="padding-left: 0px;">
                                    <div>Replacement Word</div>
                                    <input type="text" class="input" id="sasu_replacement_word" value="${SETTINGS.replacementWord}" placeholder="e.g. sports" style="max-width: 250px;" autocomplete="off">
                                    <dfn class="inputChoices-explain">Replaces "Suicide" in site headers/titles (big text like forum names, etc.). Leave empty to disable.</dfn>
                                </li>
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_remove_site_name" ${SETTINGS.removeSiteName ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Remove Site Name from Tab Title</span>
                                      <dfn class="inputChoices-explain">i.e. " | Sanctioned Suicide"</dfn>
                                    </label>
                                </li>
                            </ul>
                        </dd>
                    </dl>

                    <hr class="formRowSep">

                    <dl class="formRow">
                        <dt><div class="formRow-labelWrapper"><label class="formRow-label">Signatures</label></div></dt>
                        <dd>
                            <ul class="inputChoices" role="group">
                                <li class="inputChoices-choice" style="padding-left: 0px;">
                                    <div>Max Signature Height (px)</div>
                                    <input type="number" class="input" id="sasu_max_sig_height" value="${SETTINGS.maxSigHeight}" style="max-width: 100px;" autocomplete="off">
                                  <dfn class="inputChoices-explain">Sets a maximum height for signatures - overflow gets a scrollbar</dfn>
                                </li>
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_prevent_gif" ${SETTINGS.preventGifAutoplay ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Prevent GIF Autoplay in Signatures</span>
                                      <dfn class="inputChoices-explain">GIFs in signatures are shown as a black box until hovered</dfn>
                                    </label>
                                </li>
                            </ul>
                        </dd>
                    </dl>

                    <hr class="formRowSep">

                    <dl class="formRow">
                        <dt><div class="formRow-labelWrapper"><label class="formRow-label">Visual tweaks</label></div></dt>
                        <dd>
                            <ul class="inputChoices" role="group">
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_hide_badge" ${SETTINGS.hideBadge ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Hide Useless Chat Notification Bubble</span>
                                    </label>
                                </li>
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_normalize_chat" ${SETTINGS.normalizeChat ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Normalize Chat Text Color</span>
                                      <dfn class="inputChoices-explain">All text in chat messages is set to the default color</dfn>
                                    </label>
                                </li>
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_use_24h" ${SETTINGS.use24h ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Use 24-Hour Time Format</span>
                                    </label>
                                </li>
                            </ul>
                        </dd>
                    </dl>

                    <hr class="formRowSep">

                    <dl class="formRow">
                        <dt><div class="formRow-labelWrapper"><label class="formRow-label">Navigation</label></div></dt>
                        <dd>
                            <ul class="inputChoices" role="group">
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_jump_new" ${SETTINGS.jumpToNew ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Add "Jump to new" button to threads</span>
                                      <dfn class="inputChoices-explain">Adds a link to the latest post, even on threads previously read</dfn>
                                    </label>
                                </li>
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_report_btn" ${SETTINGS.reportButton ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Add "Report" button to profile popups</span>
                                    </label>
                                </li>
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_hide_foreign" ${SETTINGS.hideForeign ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Hide suggested threads from other subforums</span>
                                    </label>
                                </li>
                            </ul>
                        </dd>
                    </dl>

                    <hr class="formRowSep">

                    <dl class="formRow formRow--input">
                        <dt><div class="formRow-labelWrapper"><label class="formRow-label">Content filter</label></div></dt>
                        <dd>
                            <textarea class="input" id="sasu_keywords" rows="3" placeholder="e.g.:&#10;self harm&#10;cutting" autocomplete="off">${keywordString}</textarea>
                            <dfn class="inputChoices-explain">Separate keywords with new lines.</dfn>
                            <ul class="inputChoices" style="margin-top: 10px;">
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_filter_threads" ${SETTINGS.filterThreads ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Filter Thread Titles</span>
                                      <dfn class="inputChoices-explain">Threads with titles containing any filtered keywords</dfn>
                                    </label>
                                </li>
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_filter_posts" ${SETTINGS.filterPosts ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Filter Posts</span>
                                      <dfn class="inputChoices-explain">Individual posts containing any filtered keywords</dfn>
                                    </label>
                                </li>
                            </ul>
                        </dd>
                    </dl>

                    <dl class="formRow formSubmitRow">
                        <dt></dt>
                        <dd>
                            <div class="formSubmitRow-main">
                                <div class="formSubmitRow-bar"></div>
                                <div class="formSubmitRow-controls">
                                    <button type="button" id="sasu_save_btn" class="button--primary button button--icon button--icon--save rippleButton">
                                        <span class="button-text">Save Userscript Settings</span>
                                    </button>
                                </div>
                            </div>
                        </dd>
                    </dl>
                </div>
            </div>
        `;

        container.insertBefore(settingsBlock, targetForm);

        document.getElementById('sasu_save_btn').addEventListener('click', function() {
            GM_setValue('REPLACEMENT_WORD', document.getElementById('sasu_replacement_word').value);
            GM_setValue('REMOVE_SITE_NAME', document.getElementById('sasu_remove_site_name').checked);
            GM_setValue('MAX_SIGNATURE_HEIGHT', parseInt(document.getElementById('sasu_max_sig_height').value) || 150);
            GM_setValue('PREVENT_GIF_AUTOPLAY', document.getElementById('sasu_prevent_gif').checked);
            GM_setValue('HIDE_BADGE_INDICATOR', document.getElementById('sasu_hide_badge').checked);
            GM_setValue('NORMALIZE_CHAT_COLOR', document.getElementById('sasu_normalize_chat').checked);
            GM_setValue('USE_24H_TIME', document.getElementById('sasu_use_24h').checked);
            GM_setValue('ADD_JUMP_TO_NEW', document.getElementById('sasu_jump_new').checked);
            GM_setValue('ADD_REPORT_BUTTON', document.getElementById('sasu_report_btn').checked);
            GM_setValue('HIDE_FOREIGN_SUGGESTIONS', document.getElementById('sasu_hide_foreign').checked);
            GM_setValue('FILTER_THREADS', document.getElementById('sasu_filter_threads').checked);
            GM_setValue('FILTER_POSTS', document.getElementById('sasu_filter_posts').checked);

            const rawKeywords = document.getElementById('sasu_keywords').value;
            const keywordArray = rawKeywords.split('\n').map(k => k.trim()).filter(k => k.length > 0);
            GM_setValue('FILTER_KEYWORDS', keywordArray);

            const btn = this;
            btn.querySelector('.button-text').textContent = 'Saved!';
            setTimeout(() => window.location.reload(), 500);
        });
    };

    // Text replacement module
    const applyReplacementWord = () => {
        if (!SETTINGS.replacementWord.trim()) return;

        const replacerLower = SETTINGS.replacementWord.toLowerCase();
        const replacerTitle = capitalize(replacerLower);

        const replaceText = node => {
            if (['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT'].includes(node.tagName)) return;

            if (node.nodeType === Node.TEXT_NODE) {
                const originalText = node.textContent;
                const newText = originalText
                    .replace(/Suicide/g, replacerTitle)
                    .replace(/suicide/g, replacerLower);

                if (newText !== originalText) node.textContent = newText;
                return;
            }

            if (node.nodeType === Node.ELEMENT_NODE && !node.isContentEditable) {
                for (let i = 0; i < node.childNodes.length; i++) {
                    replaceText(node.childNodes[i]);
                }
            }
        };

        document.querySelectorAll('h1, h2, h3, h4, h5').forEach(header => replaceText(header));

        let newTitle = document.title
            .replace(/Suicide/g, replacerTitle)
            .replace(/suicide/g, replacerLower);
        if (newTitle !== document.title) document.title = newTitle;
    };

    const applyRemoveSiteName = () => {
        if (!SETTINGS.removeSiteName) return;
        const newTitle = document.title.replace(/[\s|]*Sanctioned Suicide/gi, '');
        if (newTitle !== document.title) document.title = newTitle;
    };

    const applyMaxSignatureHeight = () => {
        if (SETTINGS.maxSigHeight <= 0) return;
        injectCss(`
            .message-signature {
                max-height: ${SETTINGS.maxSigHeight}px !important;
                overflow-y: auto !important;
                overflow-x: hidden !important;
            }
        `);
    };

    const applyPreventGifAutoplay = () => {
        if (!SETTINGS.preventGifAutoplay) return;
        const gifSelectors = [
            '.message-signature img[src*=".gif"]',
            '.message-signature img[data-url*=".gif"]',
            '.message-signature img[alt*=".gif"]'
        ].join(', ');
        const hoverSelectors = gifSelectors.split(', ').map(s => s + ':hover').join(', ');
        injectCss(`
            .message-signature .bbImageWrapper {
                background-color: #000 !important;
                display: inline-block !important;
            }
            .bbImageWrapper:hover {
              background-color: transparent !important;
            }
            ${gifSelectors} {
                opacity: 0 !important;
            }
            ${hoverSelectors} {
                opacity: 1 !important;
            }
        `);
    }

    const applyHideBadgeIndicator = () => {
        if (!SETTINGS.hideBadge) return;
        injectCss(`
            a.p-navgroup-link--chat.badgeContainer.badgeContainer--highlighted::after {
                content: none !important;
            }
            .badge { display: none !important; }
        `);
    };

    const applyNormalizeChatColor = () => {
        if (!SETTINGS.normalizeChat) return;
        injectCss('.siropuChatMessageText .fixed-color { color: inherit !important; }');
    };

    const applyHideForeignSuggestions = () => {
        if (!SETTINGS.hideForeign) return;
        const widget = document.querySelector('[data-widget-key="xfes_thread_view_below_quick_reply_similar_threads"]');
        if (!widget) return;

        const forumLinks = Array.from(document.querySelectorAll('.p-breadcrumbs a'))
            .filter(a => a.href.includes('/forums/'));
        if (!forumLinks.length) return;

        const currentSubforum = forumLinks[forumLinks.length - 1].textContent.trim();
        widget.querySelectorAll('.structItem--thread').forEach(thread => {
            const metaLink = thread.querySelector('.structItem-parts > li > a[href*="/forums/"]');
            if (metaLink && metaLink.textContent.trim() !== currentSubforum) {
                thread.remove();
            }
        });
    };

    const apply24HourTime = (root = document) => {
        if (!SETTINGS.use24h) return;
        const rootNode = (root && root.jquery) ? root[0] : root;

        rootNode.querySelectorAll('time.u-dt').forEach(time => {
            const match = time.textContent.match(/\b(\d{1,2}):(\d{2})\s*(AM|PM)\b/i);
            if (!match) return;

            let [full, hours, minutes, period] = match;
            hours = parseInt(hours, 10);
            period = period.toUpperCase();

            if (period === 'PM' && hours < 12) hours += 12;
            if (period === 'AM' && hours === 12) hours = 0;

            time.textContent = time.textContent.replace(full, hours.toString().padStart(2, '0') + ':' + minutes);
        });
    };

    const hookXenForoTime = () => {
        if (!SETTINGS.use24h) return;
        apply24HourTime();

        const win = unsafeWindow;
        if (win.XF?.DynamicDate?.refresh) {
            const originalRefresh = win.XF.DynamicDate.refresh;
            const exportFunction = (typeof window.exportFunction === 'function') ? window.exportFunction : f => f;

            win.XF.DynamicDate.refresh = exportFunction(function(root) {
                originalRefresh.apply(this, arguments);
                apply24HourTime(root || document);
            }, win);
        }
    };

    const applyJumpToNew = () => {
        if (!SETTINGS.jumpToNew || !window.location.pathname.includes('/threads/')) return;
        const match = window.location.pathname.match(/(\/threads\/[^/]+\.\d+\/)/);
        if (!match) return;

        const targetUrl = match[1] + 'latest';
        document.querySelectorAll('.block-outer-opposite .buttonGroup').forEach(group => {
            const hasButton = Array.from(group.children).some(child =>
                child.textContent.trim() === 'Jump to new' ||
                (child.tagName === 'A' && child.href.includes('unread'))
            );
            if (!hasButton) {
                const btn = document.createElement('a');
                btn.className = 'button--link button rippleButton';
                btn.href = targetUrl;
                btn.innerHTML = '<span class="button-text">Jump to new</span>';
                group.insertBefore(btn, group.firstChild);
            }
        });
    };

    const applyReportButton = () => {
        if (!SETTINGS.reportButton) return;

        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType !== 1) continue;

                    // Search specifically for the actions container within added nodes
                    // Use querySelector because the added node might be a wrapper (e.g., .tooltip)
                    // or the container itself.
                    let actionsContainer = null;

                    if (node.classList.contains('memberTooltip-actions')) {
                        actionsContainer = node;
                    } else if (node.querySelector) {
                        actionsContainer = node.querySelector('.memberTooltip-actions');
                    }

                    if (actionsContainer && !actionsContainer.querySelector('.sasu-report-btn')) {
                        const tooltip = actionsContainer.closest('.memberTooltip');
                        if (!tooltip) continue;

                        // Find the username link to get the user ID/Slug
                        const nameLink = tooltip.querySelector('.memberTooltip-name a') || tooltip.querySelector('.memberTooltip-header a.avatar');
                        if (!nameLink) continue;

                        // Clean URL and append /report
                        const profileUrl = nameLink.href.replace(/\/+$/, '');
                        const reportUrl = `${profileUrl}/report`;

                        const reportBtn = document.createElement('a');
                        reportBtn.href = reportUrl;
                        reportBtn.className = 'button--link button sasu-report-btn';
                        reportBtn.innerHTML = '<span class="button-text">Report</span>';

                        // Append button
                        actionsContainer.appendChild(reportBtn);
                    }
                }
            }
        });

        // Use subtree: true to catch content injected into existing tooltips via AJAX
        observer.observe(document.body, { childList: true, subtree: true });
    };

    const applyKeywordFilter = () => {
        if (!SETTINGS.keywords?.length) return;
        const pattern = new RegExp(SETTINGS.keywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'i');

        if (SETTINGS.filterThreads) {
            // Thread lists
            document.querySelectorAll('.structItem--thread').forEach(thread => {
                const title = thread.querySelector('.structItem-title');
                if (title && pattern.test(title.textContent)) thread.remove();
            });

            // Sidebar nodes
            document.querySelectorAll('.node-extra').forEach(extra => {
                const title = extra.querySelector('.node-extra-title');
                if (title && pattern.test(title.textContent)) extra.remove();
            });

            // Search results
            document.querySelectorAll('.block-row').forEach(row => {
                const title = row.querySelector('.contentRow-title');
                if (title && pattern.test(title.textContent)) extra.remove();
            });
        }

        if (SETTINGS.filterPosts) {
            document.querySelectorAll('.message--post').forEach(post => {
                const content = post.querySelector('.bbWrapper');
                if (content && pattern.test(content.textContent)) post.remove();
            });
        }
    };

    const init = () => {
        injectSettingsMenu();
        applyRemoveSiteName();
        applyReplacementWord();
        applyMaxSignatureHeight();
        applyPreventGifAutoplay();
        applyHideBadgeIndicator();
        applyHideForeignSuggestions();
        applyNormalizeChatColor();
        hookXenForoTime();
        applyJumpToNew();
        applyReportButton();
        applyKeywordFilter();
    };

    if (document.readyState === 'complete') init();
    else window.addEventListener('load', init);
})();
 
  • Love
Reactions: Sunü (素女) and U. A.
U. A.

U. A.

"Ultra Based" gigashad
Aug 8, 2022
2,416
New version, largely rewritten. You'll want to just completely replace the previous version with this. Settings are now stored in a more robust way -- you can access them inside the standard SaSu preferences page, and they'll be saved across new versions without any manual editing necessary. Also, it's hosted on a proper userscript repository now, so you can automatically fetch updates if you want.

Other things were tweaked and fixed, and also added a report button per https://sanctioned-suicide.net/thre...wable-without-opening-an-entire-ticket.230961

Link: https://greasyfork.org/en/scripts/563657-sasu-improvements-script
JavaScript:
// ==UserScript==
// @name         SaSu Improvements Script
// @namespace    Violentmonkey Scripts
// @match        https://sanctioned-suicide.net/*
// @match        https://sanctionedsuicide.site/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        unsafeWindow
// @version      1.1.1
// @author       heywey
// @license      MIT
// @description  Fix some annoyances with Sanctioned Suicide forums.
// @downloadURL https://update.greasyfork.org/scripts/563657/SaSu%20Improvements%20Script.user.js
// @updateURL https://update.greasyfork.org/scripts/563657/SaSu%20Improvements%20Script.meta.js
// ==/UserScript==

(function() {
    'use strict';

    const DEFAULTS = {
        REPLACEMENT_WORD: "",
        REMOVE_SITE_NAME: false,
        MAX_SIGNATURE_HEIGHT: 150,
        PREVENT_GIF_AUTOPLAY: true,
        HIDE_BADGE_INDICATOR: true,
        NORMALIZE_CHAT_COLOR: true,
        USE_24H_TIME: false,
        ADD_JUMP_TO_NEW: true,
        ADD_REPORT_BUTTON: true,
        FILTER_KEYWORDS: [],
        HIDE_FOREIGN_SUGGESTIONS: true,
        FILTER_THREADS: true,
        FILTER_POSTS: true
    };

    const getSetting = (key) => GM_getValue(key, DEFAULTS[key]);

    const SETTINGS = {
        replacementWord: getSetting('REPLACEMENT_WORD'),
        removeSiteName: getSetting('REMOVE_SITE_NAME'),
        maxSigHeight: getSetting('MAX_SIGNATURE_HEIGHT'),
        preventGifAutoplay: getSetting('PREVENT_GIF_AUTOPLAY'),
        hideBadge: getSetting('HIDE_BADGE_INDICATOR'),
        normalizeChat: getSetting('NORMALIZE_CHAT_COLOR'),
        use24h: getSetting('USE_24H_TIME'),
        jumpToNew: getSetting('ADD_JUMP_TO_NEW'),
        reportButton: getSetting('ADD_REPORT_BUTTON'),
        keywords: getSetting('FILTER_KEYWORDS'),
        hideForeign: getSetting('HIDE_FOREIGN_SUGGESTIONS'),
        filterThreads: getSetting('FILTER_THREADS'),
        filterPosts: getSetting('FILTER_POSTS')
    };

    // Core utilities
    const injectCss = css => {
        if (!css) return;
        const style = document.createElement('style');
        style.textContent = css;
        document.head.appendChild(style);
    };

    const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1);

    // Settings menu injection
    const injectSettingsMenu = () => {
        if (!window.location.pathname.includes('/account/preferences')) return;

        const container = document.querySelector('.p-body-pageContent');
        const targetForm = document.querySelector('form[action="/account/preferences"]');
        if (!container || !targetForm) return;

        const keywordString = Array.isArray(SETTINGS.keywords) ? SETTINGS.keywords.join('\n') : '';

        const settingsBlock = document.createElement('div');
        settingsBlock.className = 'block';
        settingsBlock.style.marginBottom = '20px';

        // Add autocomplete="off" to all input fields
        settingsBlock.innerHTML = `
            <div class="block-container">
                <h2 class="block-header">SaSu Improvements Settings</h2>
                <div class="block-body">
                    <dl class="formRow">
                        <dt><div class="formRow-labelWrapper"><label class="formRow-label">Privacy</label></div></dt>
                        <dd>
                            <ul class="inputChoices" role="group">
                                <li class="inputChoices-choice" style="padding-left: 0px;">
                                    <div>Replacement Word</div>
                                    <input type="text" class="input" id="sasu_replacement_word" value="${SETTINGS.replacementWord}" placeholder="e.g. sports" style="max-width: 250px;" autocomplete="off">
                                    <dfn class="inputChoices-explain">Replaces "Suicide" in site headers/titles (big text like forum names, etc.). Leave empty to disable.</dfn>
                                </li>
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_remove_site_name" ${SETTINGS.removeSiteName ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Remove Site Name from Tab Title</span>
                                      <dfn class="inputChoices-explain">i.e. " | Sanctioned Suicide"</dfn>
                                    </label>
                                </li>
                            </ul>
                        </dd>
                    </dl>

                    <hr class="formRowSep">

                    <dl class="formRow">
                        <dt><div class="formRow-labelWrapper"><label class="formRow-label">Signatures</label></div></dt>
                        <dd>
                            <ul class="inputChoices" role="group">
                                <li class="inputChoices-choice" style="padding-left: 0px;">
                                    <div>Max Signature Height (px)</div>
                                    <input type="number" class="input" id="sasu_max_sig_height" value="${SETTINGS.maxSigHeight}" style="max-width: 100px;" autocomplete="off">
                                  <dfn class="inputChoices-explain">Sets a maximum height for signatures - overflow gets a scrollbar</dfn>
                                </li>
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_prevent_gif" ${SETTINGS.preventGifAutoplay ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Prevent GIF Autoplay in Signatures</span>
                                      <dfn class="inputChoices-explain">GIFs in signatures are shown as a black box until hovered</dfn>
                                    </label>
                                </li>
                            </ul>
                        </dd>
                    </dl>

                    <hr class="formRowSep">

                    <dl class="formRow">
                        <dt><div class="formRow-labelWrapper"><label class="formRow-label">Visual tweaks</label></div></dt>
                        <dd>
                            <ul class="inputChoices" role="group">
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_hide_badge" ${SETTINGS.hideBadge ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Hide Useless Chat Notification Bubble</span>
                                    </label>
                                </li>
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_normalize_chat" ${SETTINGS.normalizeChat ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Normalize Chat Text Color</span>
                                      <dfn class="inputChoices-explain">All text in chat messages is set to the default color</dfn>
                                    </label>
                                </li>
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_use_24h" ${SETTINGS.use24h ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Use 24-Hour Time Format</span>
                                    </label>
                                </li>
                            </ul>
                        </dd>
                    </dl>

                    <hr class="formRowSep">

                    <dl class="formRow">
                        <dt><div class="formRow-labelWrapper"><label class="formRow-label">Navigation</label></div></dt>
                        <dd>
                            <ul class="inputChoices" role="group">
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_jump_new" ${SETTINGS.jumpToNew ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Add "Jump to new" button to threads</span>
                                      <dfn class="inputChoices-explain">Adds a link to the latest post, even on threads previously read</dfn>
                                    </label>
                                </li>
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_report_btn" ${SETTINGS.reportButton ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Add "Report" button to profile popups</span>
                                    </label>
                                </li>
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_hide_foreign" ${SETTINGS.hideForeign ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Hide suggested threads from other subforums</span>
                                    </label>
                                </li>
                            </ul>
                        </dd>
                    </dl>

                    <hr class="formRowSep">

                    <dl class="formRow formRow--input">
                        <dt><div class="formRow-labelWrapper"><label class="formRow-label">Content filter</label></div></dt>
                        <dd>
                            <textarea class="input" id="sasu_keywords" rows="3" placeholder="e.g.:&#10;self harm&#10;cutting" autocomplete="off">${keywordString}</textarea>
                            <dfn class="inputChoices-explain">Separate keywords with new lines.</dfn>
                            <ul class="inputChoices" style="margin-top: 10px;">
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_filter_threads" ${SETTINGS.filterThreads ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Filter Thread Titles</span>
                                      <dfn class="inputChoices-explain">Threads with titles containing any filtered keywords</dfn>
                                    </label>
                                </li>
                                <li class="inputChoices-choice">
                                    <label class="iconic">
                                        <input type="checkbox" id="sasu_filter_posts" ${SETTINGS.filterPosts ? 'checked' : ''} autocomplete="off">
                                        <i aria-hidden="true"></i>
                                        <span class="iconic-label">Filter Posts</span>
                                      <dfn class="inputChoices-explain">Individual posts containing any filtered keywords</dfn>
                                    </label>
                                </li>
                            </ul>
                        </dd>
                    </dl>

                    <dl class="formRow formSubmitRow">
                        <dt></dt>
                        <dd>
                            <div class="formSubmitRow-main">
                                <div class="formSubmitRow-bar"></div>
                                <div class="formSubmitRow-controls">
                                    <button type="button" id="sasu_save_btn" class="button--primary button button--icon button--icon--save rippleButton">
                                        <span class="button-text">Save Userscript Settings</span>
                                    </button>
                                </div>
                            </div>
                        </dd>
                    </dl>
                </div>
            </div>
        `;

        container.insertBefore(settingsBlock, targetForm);

        document.getElementById('sasu_save_btn').addEventListener('click', function() {
            GM_setValue('REPLACEMENT_WORD', document.getElementById('sasu_replacement_word').value);
            GM_setValue('REMOVE_SITE_NAME', document.getElementById('sasu_remove_site_name').checked);
            GM_setValue('MAX_SIGNATURE_HEIGHT', parseInt(document.getElementById('sasu_max_sig_height').value) || 150);
            GM_setValue('PREVENT_GIF_AUTOPLAY', document.getElementById('sasu_prevent_gif').checked);
            GM_setValue('HIDE_BADGE_INDICATOR', document.getElementById('sasu_hide_badge').checked);
            GM_setValue('NORMALIZE_CHAT_COLOR', document.getElementById('sasu_normalize_chat').checked);
            GM_setValue('USE_24H_TIME', document.getElementById('sasu_use_24h').checked);
            GM_setValue('ADD_JUMP_TO_NEW', document.getElementById('sasu_jump_new').checked);
            GM_setValue('ADD_REPORT_BUTTON', document.getElementById('sasu_report_btn').checked);
            GM_setValue('HIDE_FOREIGN_SUGGESTIONS', document.getElementById('sasu_hide_foreign').checked);
            GM_setValue('FILTER_THREADS', document.getElementById('sasu_filter_threads').checked);
            GM_setValue('FILTER_POSTS', document.getElementById('sasu_filter_posts').checked);

            const rawKeywords = document.getElementById('sasu_keywords').value;
            const keywordArray = rawKeywords.split('\n').map(k => k.trim()).filter(k => k.length > 0);
            GM_setValue('FILTER_KEYWORDS', keywordArray);

            const btn = this;
            btn.querySelector('.button-text').textContent = 'Saved!';
            setTimeout(() => window.location.reload(), 500);
        });
    };

    // Text replacement module
    const applyReplacementWord = () => {
        if (!SETTINGS.replacementWord.trim()) return;

        const replacerLower = SETTINGS.replacementWord.toLowerCase();
        const replacerTitle = capitalize(replacerLower);

        const replaceText = node => {
            if (['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT'].includes(node.tagName)) return;

            if (node.nodeType === Node.TEXT_NODE) {
                const originalText = node.textContent;
                const newText = originalText
                    .replace(/Suicide/g, replacerTitle)
                    .replace(/suicide/g, replacerLower);

                if (newText !== originalText) node.textContent = newText;
                return;
            }

            if (node.nodeType === Node.ELEMENT_NODE && !node.isContentEditable) {
                for (let i = 0; i < node.childNodes.length; i++) {
                    replaceText(node.childNodes[i]);
                }
            }
        };

        document.querySelectorAll('h1, h2, h3, h4, h5').forEach(header => replaceText(header));

        let newTitle = document.title
            .replace(/Suicide/g, replacerTitle)
            .replace(/suicide/g, replacerLower);
        if (newTitle !== document.title) document.title = newTitle;
    };

    const applyRemoveSiteName = () => {
        if (!SETTINGS.removeSiteName) return;
        const newTitle = document.title.replace(/[\s|]*Sanctioned Suicide/gi, '');
        if (newTitle !== document.title) document.title = newTitle;
    };

    const applyMaxSignatureHeight = () => {
        if (SETTINGS.maxSigHeight <= 0) return;
        injectCss(`
            .message-signature {
                max-height: ${SETTINGS.maxSigHeight}px !important;
                overflow-y: auto !important;
                overflow-x: hidden !important;
            }
        `);
    };

    const applyPreventGifAutoplay = () => {
        if (!SETTINGS.preventGifAutoplay) return;
        const gifSelectors = [
            '.message-signature img[src*=".gif"]',
            '.message-signature img[data-url*=".gif"]',
            '.message-signature img[alt*=".gif"]'
        ].join(', ');
        const hoverSelectors = gifSelectors.split(', ').map(s => s + ':hover').join(', ');
        injectCss(`
            .message-signature .bbImageWrapper {
                background-color: #000 !important;
                display: inline-block !important;
            }
            .bbImageWrapper:hover {
              background-color: transparent !important;
            }
            ${gifSelectors} {
                opacity: 0 !important;
            }
            ${hoverSelectors} {
                opacity: 1 !important;
            }
        `);
    }

    const applyHideBadgeIndicator = () => {
        if (!SETTINGS.hideBadge) return;
        injectCss(`
            a.p-navgroup-link--chat.badgeContainer.badgeContainer--highlighted::after {
                content: none !important;
            }
            .badge { display: none !important; }
        `);
    };

    const applyNormalizeChatColor = () => {
        if (!SETTINGS.normalizeChat) return;
        injectCss('.siropuChatMessageText .fixed-color { color: inherit !important; }');
    };

    const applyHideForeignSuggestions = () => {
        if (!SETTINGS.hideForeign) return;
        const widget = document.querySelector('[data-widget-key="xfes_thread_view_below_quick_reply_similar_threads"]');
        if (!widget) return;

        const forumLinks = Array.from(document.querySelectorAll('.p-breadcrumbs a'))
            .filter(a => a.href.includes('/forums/'));
        if (!forumLinks.length) return;

        const currentSubforum = forumLinks[forumLinks.length - 1].textContent.trim();
        widget.querySelectorAll('.structItem--thread').forEach(thread => {
            const metaLink = thread.querySelector('.structItem-parts > li > a[href*="/forums/"]');
            if (metaLink && metaLink.textContent.trim() !== currentSubforum) {
                thread.remove();
            }
        });
    };

    const apply24HourTime = (root = document) => {
        if (!SETTINGS.use24h) return;
        const rootNode = (root && root.jquery) ? root[0] : root;

        rootNode.querySelectorAll('time.u-dt').forEach(time => {
            const match = time.textContent.match(/\b(\d{1,2}):(\d{2})\s*(AM|PM)\b/i);
            if (!match) return;

            let [full, hours, minutes, period] = match;
            hours = parseInt(hours, 10);
            period = period.toUpperCase();

            if (period === 'PM' && hours < 12) hours += 12;
            if (period === 'AM' && hours === 12) hours = 0;

            time.textContent = time.textContent.replace(full, hours.toString().padStart(2, '0') + ':' + minutes);
        });
    };

    const hookXenForoTime = () => {
        if (!SETTINGS.use24h) return;
        apply24HourTime();

        const win = unsafeWindow;
        if (win.XF?.DynamicDate?.refresh) {
            const originalRefresh = win.XF.DynamicDate.refresh;
            const exportFunction = (typeof window.exportFunction === 'function') ? window.exportFunction : f => f;

            win.XF.DynamicDate.refresh = exportFunction(function(root) {
                originalRefresh.apply(this, arguments);
                apply24HourTime(root || document);
            }, win);
        }
    };

    const applyJumpToNew = () => {
        if (!SETTINGS.jumpToNew || !window.location.pathname.includes('/threads/')) return;
        const match = window.location.pathname.match(/(\/threads\/[^/]+\.\d+\/)/);
        if (!match) return;

        const targetUrl = match[1] + 'latest';
        document.querySelectorAll('.block-outer-opposite .buttonGroup').forEach(group => {
            const hasButton = Array.from(group.children).some(child =>
                child.textContent.trim() === 'Jump to new' ||
                (child.tagName === 'A' && child.href.includes('unread'))
            );
            if (!hasButton) {
                const btn = document.createElement('a');
                btn.className = 'button--link button rippleButton';
                btn.href = targetUrl;
                btn.innerHTML = '<span class="button-text">Jump to new</span>';
                group.insertBefore(btn, group.firstChild);
            }
        });
    };

    const applyReportButton = () => {
        if (!SETTINGS.reportButton) return;

        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType !== 1) continue;

                    // Search specifically for the actions container within added nodes
                    // Use querySelector because the added node might be a wrapper (e.g., .tooltip)
                    // or the container itself.
                    let actionsContainer = null;

                    if (node.classList.contains('memberTooltip-actions')) {
                        actionsContainer = node;
                    } else if (node.querySelector) {
                        actionsContainer = node.querySelector('.memberTooltip-actions');
                    }

                    if (actionsContainer && !actionsContainer.querySelector('.sasu-report-btn')) {
                        const tooltip = actionsContainer.closest('.memberTooltip');
                        if (!tooltip) continue;

                        // Find the username link to get the user ID/Slug
                        const nameLink = tooltip.querySelector('.memberTooltip-name a') || tooltip.querySelector('.memberTooltip-header a.avatar');
                        if (!nameLink) continue;

                        // Clean URL and append /report
                        const profileUrl = nameLink.href.replace(/\/+$/, '');
                        const reportUrl = `${profileUrl}/report`;

                        const reportBtn = document.createElement('a');
                        reportBtn.href = reportUrl;
                        reportBtn.className = 'button--link button sasu-report-btn';
                        reportBtn.innerHTML = '<span class="button-text">Report</span>';

                        // Append button
                        actionsContainer.appendChild(reportBtn);
                    }
                }
            }
        });

        // Use subtree: true to catch content injected into existing tooltips via AJAX
        observer.observe(document.body, { childList: true, subtree: true });
    };

    const applyKeywordFilter = () => {
        if (!SETTINGS.keywords?.length) return;
        const pattern = new RegExp(SETTINGS.keywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'i');

        if (SETTINGS.filterThreads) {
            // Thread lists
            document.querySelectorAll('.structItem--thread').forEach(thread => {
                const title = thread.querySelector('.structItem-title');
                if (title && pattern.test(title.textContent)) thread.remove();
            });

            // Sidebar nodes
            document.querySelectorAll('.node-extra').forEach(extra => {
                const title = extra.querySelector('.node-extra-title');
                if (title && pattern.test(title.textContent)) extra.remove();
            });

            // Search results
            document.querySelectorAll('.block-row').forEach(row => {
                const title = row.querySelector('.contentRow-title');
                if (title && pattern.test(title.textContent)) extra.remove();
            });
        }

        if (SETTINGS.filterPosts) {
            document.querySelectorAll('.message--post').forEach(post => {
                const content = post.querySelector('.bbWrapper');
                if (content && pattern.test(content.textContent)) post.remove();
            });
        }
    };

    const init = () => {
        injectSettingsMenu();
        applyRemoveSiteName();
        applyReplacementWord();
        applyMaxSignatureHeight();
        applyPreventGifAutoplay();
        applyHideBadgeIndicator();
        applyHideForeignSuggestions();
        applyNormalizeChatColor();
        hookXenForoTime();
        applyJumpToNew();
        applyReportButton();
        applyKeywordFilter();
    };

    if (document.readyState === 'complete') init();
    else window.addEventListener('load', init);
})();
Hidden content
You need -1 more posts to view this content
 
U. A.

U. A.

"Ultra Based" gigashad
Aug 8, 2022
2,416
Hidden content
You need -1 more posts to view this content
 
  • Love
  • Hugs
  • Like
Reactions: Always-in-trouble, OnceTheHappiestMan, webb&flow and 1 other person