>what does broot even do herehe doesn't do anything with his admin role, nothing fun, he doesn't host any events anymore, nobody actually likes him besides the approvers who want janny, and he doesn't even want to make any connections with the people here, he is literally the same as he was on the shroom here, literal cosmetic admin. Just give his role back to DOLL or root atp.
  • Welcome. Registering and having your account be approved may take a while since there's only one guy checking and approving accounts and guestposts
  • Boards are being moved around and added. Leave feedback in this thread and let jannies move your threads

Userscripts! Userscripts? Userscripts.

Got this hunk of junk working just a moment ago that adds 'ru-esque instant thread updates. This removes the need to refresh and check notifications as to update threads.
Fixed.
JavaScript:
// ==UserScript==
// @name         Soyjak.forum Live New Posts
// @namespace    https://greasyfork.org/
// @version      8.1.1
// @description  Polls XenForo for new thread posts every 5 seconds on the newest page only.
// @author       You
// @match        https://soyjak.forum/threads/*
// @match        https://*.soyjak.forum/threads/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function(){'use strict';

const INTERVAL=5000;
let busy=false;

const log=(...args)=>console.log('[Soyjak Live]',...args);
const getPosts=()=>[...document.querySelectorAll('.message.message--post')];

function getLastTimestamp(){
    const posts=getPosts();
    if(!posts.length)return null;
    const time=posts.at(-1).querySelector('time[data-timestamp]');
    if(!time){
        log('No timestamp found in last post:',posts.at(-1));
        return null;
    }
    const timestamp=Number(time.getAttribute('data-timestamp'));
    return Number.isFinite(timestamp)?timestamp:null;
}

function getCurrentPage(){
    const match=location.pathname.match(/\/page-(\d+)\/?$/);
    return match?Number(match[1]):1;
}

function getPageNumber(href){
    if(!href)return null;
    try{
        const url=new URL(href,location.href);
        const match=url.pathname.match(/\/page-(\d+)\/?$/);
        return match?Number(match[1]):1;
    }catch{
        return null;
    }
}

function getLastPage(){
    const current=getCurrentPage();
    const pages=[];

    document.querySelectorAll('.pageNav a[href]').forEach(link=>{
        const page=getPageNumber(link.getAttribute('href'));
        if(Number.isFinite(page))pages.push(page);
    });

    document.querySelectorAll('.pageNav-page,.pageNav-page--current').forEach(el=>{
        const text=el.textContent.trim();
        if(/^\d+$/.test(text))pages.push(Number(text));
    });

    return pages.length?Math.max(current,...pages):current;
}

function isLastPage(){
    const current=getCurrentPage();
    const last=getLastPage();
    log(`Pagination: current page ${current}, detected last page ${last}`);
    return current>=last;
}

function getNewPostsURL(after){
    const url=new URL(location.href);
    url.pathname=url.pathname.replace(/\/page-\d+\/?$/,'/');
    url.pathname=url.pathname.replace(/\/$/,'/new-posts');
    url.search='';
    url.searchParams.set('after',after);
    return url.href;
}

async function poll(){
    if(busy||!isLastPage())return;

    const after=getLastTimestamp();
    if(!after){
        log('Could not determine last post timestamp.');
        return;
    }

    busy=true;

    try{
        const url=getNewPostsURL(after);
        log('Checking:',url);

        const response=await fetch(url,{
            method:'GET',
            credentials:'same-origin',
            cache:'no-store',
            headers:{'X-Requested-With':'XMLHttpRequest'}
        });

        if(!response.ok)throw new Error(`HTTP ${response.status}`);

        const doc=new DOMParser().parseFromString(
            await response.text(),
            'text/html'
        );

        const newPosts=[
            ...doc.querySelectorAll('.message.message--post')
        ];

        if(!newPosts.length){
            log('No new posts.');
            return;
        }

        const container=document.querySelector('.block--messages');
        if(!container){
            log('Could not find message container.');
            return;
        }

        const existingIds=new Set(getPosts().map(post=>post.id));
        const freshPosts=newPosts.filter(post=>!existingIds.has(post.id));

        if(!freshPosts.length){
            log('Posts were already present.');
            return;
        }

        const atBottom=
            window.innerHeight+window.scrollY>=
            document.documentElement.scrollHeight-500;

        let insertAfter=getPosts().at(-1);

        for(const post of freshPosts){
            const imported=document.importNode(post,true);

            if(insertAfter)
                insertAfter.insertAdjacentElement('afterend',imported);
            else
                container.appendChild(imported);

            if(typeof XF!=='undefined'&&typeof XF.activate==='function')
                XF.activate(imported);

            insertAfter=imported;
        }

        log(`Inserted ${freshPosts.length} new post(s).`);

        if(atBottom)
            window.scrollTo({
                top:document.documentElement.scrollHeight,
                behavior:'smooth'
            });

    }catch(error){
        log('Error:',error);
    }finally{
        busy=false;
    }
}

if(!document.querySelector('.message.message--post'))return;

if(!isLastPage()){
    log('Not on the newest page. Live polling disabled.');
    return;
}

log('Started. Polling every 5 seconds.');
setTimeout(poll,1000);
setInterval(poll,INTERVAL);

})();
 
Last edited:
30 seconds might be to long tbh. Here is a 5 second one.
JavaScript:
// ==UserScript==
// @name         Soyjak.forum Auto-Refresh Notifications
// @namespace    https://greasyfork.org/
// @version      1.5
// @description  Refreshes the notification badge every 5 seconds on soyjak.forum without marking alerts as read
// @author       You
// @match        *://soyjak.forum/*
// @match        *://*.soyjak.forum/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    const INTERVAL_MS = 5000; // 5 seconds

    function isXenForo() {
        return (
            (typeof XF !== 'undefined' && XF.ajax) ||
            (typeof XenForo !== 'undefined' && XenForo.ajax) ||
            document.documentElement?.id === 'XF' ||
            document.documentElement?.id === 'XenForo'
        );
    }

    function poll() {
        // Prefer modern XF2
        if (typeof XF !== 'undefined' && XF.ajax && XF.canonicalizeUrl) {
            // Very light endpoint that still returns the visitor counters
            // (does NOT open or mark alerts as read)
            XF.ajax(
                'GET',
                XF.canonicalizeUrl('index.php?whats-new/latest-activity'),
                {},
                null,
                {
                    global: false,        // no spinner
                    skipDefault: true,
                    error: function () {}
                }
            );
            return;
        }

        // Fallback for older XenForo
        if (typeof XenForo !== 'undefined' && XenForo.ajax) {
            XenForo.ajax(
                'index.php?misc/lightbox',
                {},
                function () {},
                {
                    global: false,
                    error: function () {}
                }
            );
        }
    }

    if (!isXenForo()) return;

    setTimeout(() => {
        poll();
        setInterval(poll, INTERVAL_MS);
    }, 2000);
})();
There's also this by Idkchud which does the same with notifications.
 
changes youtube embeds to invidious embeds like in old December
JavaScript:
// ==UserScript==
// @name         Soyjak.Forum — YouTube → Invidious
// @namespace    https://soyjak.forum/
// @version      1.0.0
// @description  Rewrites every YouTube embed to invidious.nerdvpn.de. Handles iframes, lazy-loaded embeds, nocookie, and JS-set srcs.
// @author       -
// @match        https://soyjak.forum/*
// @match        https://*.soyjak.forum/*
// @run-at       document-start
// @grant        none
// @noframes
// ==/UserScript==

(function () {
    'use strict';

    /* ------------------------------------------------------------------ */
    /*  Config                                                             */
    /* ------------------------------------------------------------------ */
    const INVIDIOUS = 'https://yt.omada.cafe';

    // Hosts we consider "YouTube". Everything here gets rewritten.
    const YT_HOSTS = new Set([
        'youtube.com',
        'www.youtube.com',
        'm.youtube.com',
        'music.youtube.com',
        'youtube-nocookie.com',
        'www.youtube-nocookie.com',
        'youtu.be',
        'www.youtu.be',
    ]);

    /* ------------------------------------------------------------------ */
    /*  URL → Invidious rewrite                                            */
    /* ------------------------------------------------------------------ */
    function rewriteUrl(raw) {
        if (!raw) return null;
        if (raw.startsWith(INVIDIOUS)) return null; // already rewritten

        let url;
        try {
            url = new URL(raw, location.href);
        } catch (e) {
            return null;
        }

        if (!YT_HOSTS.has(url.hostname.toLowerCase())) return null;

        let videoId = null;
        let start   = url.searchParams.get('start') || url.searchParams.get('t');

        // youtu.be/VIDEO_ID
        if (url.hostname.endsWith('youtu.be')) {
            videoId = url.pathname.slice(1).split('/')[0];
        }
        // /embed/VIDEO_ID, /v/VIDEO_ID, /shorts/VIDEO_ID, /live/VIDEO_ID
        else {
            const m = url.pathname.match(/^\/(embed|v|shorts|live)\/([A-Za-z0-9_-]{6,})/);
            if (m) {
                videoId = m[2];
            } else {
                // /watch?v=VIDEO_ID
                videoId = url.searchParams.get('v');
            }
        }

        if (!videoId) return null;

        const out = new URL(`${INVIDIOUS}/embed/${videoId}`);
        if (start) {
            // Normalize "1h2m3s" / "90s" / "90" → seconds
            const seconds = parseTime(start);
            if (seconds != null) out.searchParams.set('start', String(seconds));
        }
        return out.toString();
    }

    function parseTime(t) {
        if (/^\d+$/.test(t)) return parseInt(t, 10);
        const m = t.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/);
        if (!m) return null;
        const [, h, mm, s] = m;
        return (parseInt(h || 0, 10) * 3600)
             + (parseInt(mm || 0, 10) * 60)
             + (parseInt(s  || 0, 10));
    }

    /* ------------------------------------------------------------------ */
    /*  Rewrite helpers                                                    */
    /* ------------------------------------------------------------------ */
    const handledIframes = new WeakSet();

    function rewriteIframe(frame) {
        if (!(frame instanceof HTMLIFrameElement)) return;

        // Handle both `src` and lazy `data-src` variants.
        for (const attr of ['src', 'data-src', 'data-lazy-src']) {
            const current = frame.getAttribute(attr);
            if (!current) continue;
            const next = rewriteUrl(current);
            if (next) frame.setAttribute(attr, next);
        }

        // Mark so we don't re-scan needlessly.
        if (!handledIframes.has(frame)) handledIframes.add(frame);
    }

    function rewriteAll(root) {
        if (!root || !root.querySelectorAll) return;
        const frames = root.querySelectorAll(
            'iframe[src], iframe[data-src], iframe[data-lazy-src]'
        );
        for (let i = 0; i < frames.length; i++) rewriteIframe(frames[i]);
    }

    /* ------------------------------------------------------------------ */
    /*  Catch every iframe the parser or JS creates                        */
    /* ------------------------------------------------------------------ */
    const mo = new MutationObserver(muts => {
        for (const m of muts) {
            if (m.type === 'childList') {
                for (const n of m.addedNodes) {
                    if (n.nodeType !== 1) continue;
                    if (n.tagName === 'IFRAME') rewriteIframe(n);
                    else rewriteAll(n);
                }
            } else if (m.type === 'attributes' && m.target.tagName === 'IFRAME') {
                rewriteIframe(m.target);
            }
        }
    });

    function startObserver() {
        if (!document.documentElement) {
            requestAnimationFrame(startObserver);
            return;
        }
        mo.observe(document.documentElement, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['src', 'data-src', 'data-lazy-src'],
        });
        rewriteAll(document.documentElement);
    }
    startObserver();

    /* ------------------------------------------------------------------ */
    /*  Intercept the YouTube IFrame API for players created via JS        */
    /*  (XenForo's media embed occasionally uses this path)                */
    /* ------------------------------------------------------------------ */
    // Many lazy embed systems just set `iframe.src = '...'` directly.
    // The MutationObserver above already catches that via the attribute
    // filter, so no need to monkey-patch HTMLIFrameElement.src.

    /* ------------------------------------------------------------------ */
    /*  Catch <a> links that point at YouTube and are embedded inline      */
    /*  (optional — comment out if you don't want links rewritten)         */
    /* ------------------------------------------------------------------ */
    // Intentionally NOT rewriting <a href> links — this script is only
    // for *embeds*. Uncomment below if you want every YouTube link
    // navigated to Invidious too.
    /*
    function rewriteLinks(root) {
        if (!root || !root.querySelectorAll) return;
        const links = root.querySelectorAll('a[href]');
        for (const a of links) {
            const next = rewriteUrl(a.getAttribute('href'));
            if (next) a.setAttribute('href', next);
        }
    }
    */

    /* ------------------------------------------------------------------ */
    /*  Final sweep — XenForo sometimes injects embeds after `load`        */
    /* ------------------------------------------------------------------ */
    window.addEventListener('load', () => rewriteAll(document));
    setInterval(() => rewriteAll(document), 2000);
})();
 
@X3D3XD
This will make suppressing things much much more easy and not as manual
JavaScript:
// ==UserScript==
// @name         SNCapedia Shift-Click Range Select
// @namespace    https://sncapedia.org/
// @version      1.0.0
// @description  Shift-click a checkbox to tick/untick everything between it and the last checkbox you clicked. Works on page history, Special:Contributions, recent changes, logs, watchlists, etc. — handy for mass revision-delete / suppress.
// @match        https://sncapedia.org/*
// @match        https://www.sncapedia.org/*
// @match        http://sncapedia.org/*
// @match        http://www.sncapedia.org/*
// @run-at       document-idle
// @grant        none
// @noframes
// ==/UserScript==

(function () {
    'use strict';

    /* ------------------------------------------------------------------ *
     *  Containers whose checkboxes should be treated as one selectable
     *  list.  Add your own selector here if some special page is missing.
     * ------------------------------------------------------------------ */
    const CONTAINER_SELECTORS = [
        '#pagehistory',                  // Page history
        '#mw-history-compare',           // newer skins wrap the list here too
        '.mw-contributions-list',        // Special:Contributions / DeletedContributions
        '.mw-changeslist',               // Recent changes, Watchlist, Related changes
        '.mw-recentchanges',             // newer RC markup
        '#mw-watchlist-form',            // Special:Watchlist
        '.mw-logevent-loglines',         // Special:Log / suppression log
        '.mw-revisionlist',              // Special:RevisionDelete listings
        '.mw-htmlform-field-HTMLMultiSelectField'
    ].join(',');

    /* ----------------------------- helpers ---------------------------- */

    const isVisible = (el) =>
        !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);

    function findContainer(cb) {
        const known = cb.closest(CONTAINER_SELECTORS);
        if (known) return known;

        // Fallback: climb until we reach an ancestor holding 2+ checkboxes.
        let el = cb.parentElement;
        while (el && el !== document.body) {
            if (el.querySelectorAll('input[type="checkbox"]').length > 1) return el;
            el = el.parentElement;
        }
        return null;
    }

    function boxesIn(container) {
        return Array.from(container.querySelectorAll('input[type="checkbox"]'))
            .filter((cb) => !cb.disabled && isVisible(cb));
    }

    /* ------------------------------ state ----------------------------- */

    let anchor = null; // last checkbox clicked WITHOUT Shift

    /* ----------------------------- handler ---------------------------- */

    document.addEventListener('click', (event) => {
        if (event.button !== 0) return; // left click only

        let cb = event.target;

        // Direct hit on a real checkbox?
        if (!(cb instanceof HTMLInputElement && cb.type === 'checkbox')) {
            // OOUI-style checkbox: the real <input> is hidden inside a widget.
            const widget = cb instanceof Element
                ? cb.closest('.oo-ui-checkboxInputWidget')
                : null;
            cb = widget ? widget.querySelector('input[type="checkbox"]') : null;
            if (!cb) return; // labels re-dispatch a click on the input anyway
        }

        const container = findContainer(cb);
        if (!container) {
            anchor = null;
            return;
        }

        // Plain click, or Shift-click with no usable anchor → set a new anchor.
        if (!event.shiftKey || !anchor || findContainer(anchor) !== container) {
            anchor = cb;
            return;
        }

        const boxes = boxesIn(container);
        const from = boxes.indexOf(anchor);
        const to = boxes.indexOf(cb);

        if (from === -1 || to === -1) {
            anchor = cb;
            return;
        }

        const [lo, hi] = from <= to ? [from, to] : [to, from];
        const state = cb.checked; // browser already toggled the clicked box

        for (let i = lo; i <= hi; i++) {
            const box = boxes[i];
            if (box.checked === state) continue;
            box.checked = state;
            // Tell MediaWiki (and anything else listening) that it changed,
            // so "Compare selected revisions" / visibility buttons enable.
            box.dispatchEvent(new Event('input', { bubbles: true }));
            box.dispatchEvent(new Event('change', { bubbles: true }));
        }

        // The anchor deliberately stays put, so repeated shift-clicks just
        // move the far end of the range.  Uncomment the next line if you'd
        // rather each shift-click become the new anchor:
        // anchor = cb;
    }, false);
})();
 
Back
Top Bottom