Bonk.io – Mute/Hide Toxic Users (Safe Client-side)

Adds buttons to Mute/Unmute users in Bonk.io chat and hides messages containing offensive/banned words. Saves list in localStorage. Muted users appear in red, report links to Bonk Discord.

// ==UserScript==
// @name         Bonk.io – Mute/Hide Toxic Users (Safe Client-side)
// @namespace    https://github.com/thebestg5
// @version      1.2.0
// @description  Adds buttons to Mute/Unmute users in Bonk.io chat and hides messages containing offensive/banned words. Saves list in localStorage. Muted users appear in red, report links to Bonk Discord.
// @author       thebestg5
// @match        https://bonk.io/*
// @match        https://www.bonk.io/*
// @run-at       document-end
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const STORAGE_KEYS = {
        muted: 'bonk_mute_list_v1',
        badwords: 'bonk_badwords_v1'
    };

    const DEFAULT_BADWORDS = [
        'idiot','imbecile','stupid','moron','dumb','bastard','shit','fuck','noob','stfu','kys'
    ];

    const state = {
        muted: new Set(loadJSON(STORAGE_KEYS.muted, [])),
        badwords: new Set(loadJSON(STORAGE_KEYS.badwords, DEFAULT_BADWORDS))
    };

    function saveSets() {
        saveJSON(STORAGE_KEYS.muted, Array.from(state.muted));
        saveJSON(STORAGE_KEYS.badwords, Array.from(state.badwords));
    }

    function loadJSON(key,fallback){try{const raw=localStorage.getItem(key);return raw?JSON.parse(raw):fallback}catch{return fallback}}
    function saveJSON(key,value){try{localStorage.setItem(key,JSON.stringify(value))}catch{}}

    function createControlPanel() {
        const panel = document.createElement('div');
        panel.id = 'bonk-mute-panel';
        Object.assign(panel.style,{
            position:'fixed',right:'12px',bottom:'12px',zIndex:99999,
            background:'rgba(0,0,0,0.75)',color:'#fff',padding:'10px 12px',
            borderRadius:'12px',fontFamily:'sans-serif',fontSize:'12px',maxWidth:'320px',backdropFilter:'blur(2px)'
        });

        panel.innerHTML = `
        <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
            <strong style="font-size:13px;">Bonk Mute Helper</strong>
            <button id="bm-toggle" title="Hide/Show" style="margin-left:auto;">−</button>
        </div>
        <div id="bm-body">
            <div style="margin-bottom:8px;opacity:0.9">Click <em>Mute</em> next to a username in chat to add them to the list.</div>
            <div style="display:flex;gap:6px;margin-bottom:8px;">
                <input id="bm-user" placeholder="Username" style="flex:1;padding:4px;border-radius:6px;border:1px solid #333;background:#111;color:#fff;" />
                <button id="bm-add-user">Add</button>
            </div>
            <div style="margin-bottom:8px;">
                <div style="margin-bottom:4px;opacity:0.9">Muted users</div>
                <div id="bm-users" style="display:flex;flex-wrap:wrap;gap:6px;"></div>
            </div>
            <hr style="border:none;border-top:1px solid #333;margin:8px 0;"/>
            <div style="margin-bottom:6px;opacity:0.9">Banned words (comma-separated)</div>
            <textarea id="bm-badwords" rows="3" style="width:100%;padding:6px;border-radius:6px;border:1px solid #333;background:#111;color:#fff;"></textarea>
            <div style="display:flex;gap:6px;margin-top:6px;">
                <button id="bm-save-bw">Save words</button>
                <button id="bm-reset-bw" title="Reset to default list">Reset</button>
            </div>
        </div>`;

        document.body.appendChild(panel);

        const body = panel.querySelector('#bm-body');
        const toggleBtn = panel.querySelector('#bm-toggle');
        toggleBtn.addEventListener('click', ()=>{
            const hidden = body.style.display==='none';
            body.style.display = hidden?'':'none';
            toggleBtn.textContent = hidden?'−':'+';
        });

        const userInput = panel.querySelector('#bm-user');
        panel.querySelector('#bm-add-user').addEventListener('click',()=>{
            const name=(userInput.value||'').trim();
            if(!name) return;
            state.muted.add(name);
            saveSets();
            userInput.value='';
            renderMutedUsers();
        });

        const ta = panel.querySelector('#bm-badwords');
        ta.value = Array.from(state.badwords).join(', ');
        panel.querySelector('#bm-save-bw').addEventListener('click',()=>{
            const parts=ta.value.split(',').map(s=>s.trim()).filter(Boolean);
            state.badwords = new Set(parts);
            saveSets();
            alert('Words saved!');
        });
        panel.querySelector('#bm-reset-bw').addEventListener('click',()=>{
            state.badwords = new Set(DEFAULT_BADWORDS);
            saveSets();
            ta.value = Array.from(state.badwords).join(', ');
        });

        function renderMutedUsers(){
            const wrap = panel.querySelector('#bm-users');
            wrap.innerHTML='';
            Array.from(state.muted).sort((a,b)=>a.localeCompare(b)).forEach(name=>{
                const chip = document.createElement('span');
                chip.textContent = name;
                chip.style.color='red';
                Object.assign(chip.style,{
                    padding:'4px 8px',borderRadius:'999px',background:'#222',
                    border:'1px solid #333',display:'inline-flex',gap:'6px',alignItems:'center'
                });

                const x = document.createElement('button');
                x.textContent='✕';x.title='Unmute';
                Object.assign(x.style,{marginLeft:'6px',cursor:'pointer'});
                x.addEventListener('click',()=>{
                    state.muted.delete(name);
                    saveSets();
                    renderMutedUsers();
                });
                chip.appendChild(x);

                const reportBtn = document.createElement('button');
                reportBtn.textContent='[Report]';
                Object.assign(reportBtn.style,{marginLeft:'6px',cursor:'pointer',fontSize:'11px',background:'transparent',color:'#f99',border:'none'});
                reportBtn.addEventListener('click',()=>window.open('https://discord.gg/bonk','_blank'));
                chip.appendChild(reportBtn);

                wrap.appendChild(chip);
            });
        }

        renderMutedUsers();
    }

    function containsBadword(text){
        const t=(text||'').toLowerCase();
        for(const w of state.badwords){
            if(!w) continue;
            if(t.includes(w.toLowerCase())) return true;
        }
        return false;
    }

    function enhanceChat(){
        const candidates = Array.from(document.querySelectorAll('*'))
        .filter(el=>{
            const id=(el.id||'').toLowerCase();
            const cls=(el.className||'').toString().toLowerCase();
            const looksLikeChat = id.includes('chat') || cls.includes('chat');
            const manyLines = el.childElementCount>3 && el.scrollHeight>100;
            return looksLikeChat && manyLines;
        });
        const chatBox = candidates[0] || document.body;
        const processed = new WeakSet();

        const observer = new MutationObserver(muts=>{
            muts.forEach(m=>{
                m.addedNodes.forEach(node=>{
                    if(!(node instanceof HTMLElement)) return;
                    const lineEls = node.querySelectorAll? [node,...node.querySelectorAll('*')]:[node];
                    lineEls.forEach(el=>tryProcessLine(el));
                });
            });
        });
        observer.observe(chatBox,{childList:true,subtree:true});

        function tryProcessLine(el){
            if(processed.has(el)) return;
            const text = el.textContent||'';
            const idx = text.indexOf(':');
            if(idx<=0||idx>30) return;
            const name = text.slice(0,idx).trim();
            const message = text.slice(idx+1).trim();
            if(!name||!message) return;
            processed.add(el);

            if(state.muted.has(name)||containsBadword(message)) el.style.display='none';

            const btnWrap=document.createElement('span');btnWrap.style.marginLeft='8px';
            const muteBtn=document.createElement('button');
            muteBtn.textContent = state.muted.has(name)?'[Unmute]':'[Mute]';
            Object.assign(muteBtn.style,{cursor:'pointer',fontSize:'11px',background:'transparent',color:'#9cf',border:'none'});
            muteBtn.addEventListener('click',ev=>{
                ev.stopPropagation();
                if(state.muted.has(name)) state.muted.delete(name); else state.muted.add(name);
                saveSets();
                muteBtn.textContent=state.muted.has(name)?'[Unmute]':'[Mute]';
                el.style.display = state.muted.has(name)?'none':'';
            });
            btnWrap.appendChild(muteBtn);

            const reportBtn = document.createElement('button');
            reportBtn.textContent='[Report]';
            Object.assign(reportBtn.style,{marginLeft:'6px',cursor:'pointer',fontSize:'11px',background:'transparent',color:'#f99',border:'none'});
            reportBtn.addEventListener('click',()=>window.open('https://discord.gg/bonk','_blank'));
            btnWrap.appendChild(reportBtn);

            el.appendChild(btnWrap);
        }
    }

    function hideMutedInLobby(){
        const observer = new MutationObserver(()=>{
            const nameNodes = Array.from(document.querySelectorAll('div,span,li'))
            .filter(el=>el.childElementCount===0 && el.textContent && el.textContent.length<=20);
            nameNodes.forEach(el=>{
                const name=el.textContent.trim();
                if(state.muted.has(name)) el.style.opacity='0.35';
            });
        });
        observer.observe(document.body,{childList:true,subtree:true});
    }

    function init(){
        createControlPanel();
        enhanceChat();
        hideMutedInLobby();
        console.log('[Bonk Mute Helper] active');
    }

    if(document.readyState==='loading'){
        document.addEventListener('DOMContentLoaded',init);
    } else init();
})();