Greasy Fork is available in English.

Edit EVERY message!(discord)

Same code(from function to end) can also work on Discord Desktop if you open devtools and run it on Console

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Advertisement:

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

Advertisement:

// ==UserScript==
// @name         Edit EVERY message!(discord)
// @namespace    https://example.org
// @license      MIT
// @version      0.1.3
// @description  Same code(from function to end) can also work on Discord Desktop if you open devtools and run it on Console
// @author       Lio
// @match        https://discord.com/*
// @grant        none
// @noframes
// ==/UserScript==

(function () {
    "use strict";

    const EDIT_BUTTON = "local-edit-button";
    const CONTROL_BOX = "local-edit-controls";
    const STYLE_ID = "local-edit-styles";

    let observer = null;
    let editButtonsHidden = false;

    // Opens an inline textarea editor in place of `content`.
    // `compact` is used for the small quoted-reply snippet, which lives in
    // a narrow, height-clipped single-line row and breaks if given the
    // full-size editor without first relaxing that row's constraints.
    function startEdit(content, compact) {

        if (!content.dataset.originalText) {
            content.dataset.originalText = content.innerText;
        }

        // The reply row (and sometimes its parent too) is usually styled
        // with overflow:hidden / white-space:nowrap / a fixed height to
        // keep the quoted snippet to one truncated line. Save their
        // current inline styles and relax those constraints for as long
        // as we're editing, restoring them afterwards.
        const relaxedAncestors = [];

        if (compact) {
            let cur = content.parentElement;
            let levels = 0;

            while (cur && levels < 3) {
                relaxedAncestors.push({
                    el: cur,
                    cssText: cur.style.cssText
                });

                cur.style.overflow = "visible";
                cur.style.whiteSpace = "normal";
                cur.style.height = "auto";
                cur.style.maxHeight = "none";
                cur.style.textOverflow = "clip";
                cur.style.alignItems = "flex-start";

                cur = cur.parentElement;
                levels++;
            }
        }

        function restoreAncestors() {
            relaxedAncestors.forEach(({ el, cssText }) => {
                el.style.cssText = cssText;
            });
        }

        const textarea = document.createElement("textarea");
        textarea.value = content.innerText;

        textarea.style.cssText = compact ? `
            display:block;
            width:100%;
            min-width:0;
            min-height:20px;
            max-height:160px;
            background:#2b2d31;
            color:#dbdee1;
            font-size:13.5px;
            line-height:1.3;
            font-family:inherit;
            border:1px solid #5865F2;
            border-radius:4px;
            padding:2px 6px;
            resize:vertical;
            box-sizing:border-box;
        ` : `
            display:block;
            width:100%;
            min-width:0;
            min-height:45px;
            background:#2b2d31;
            color:white;
            border:1px solid #5865F2;
            border-radius:5px;
            padding:6px;
            resize:vertical;
            box-sizing:border-box;
        `;

        const save = document.createElement("button");
        save.textContent = "Save";

        const cancel = document.createElement("button");
        cancel.textContent = "Cancel";

        const btnStyle = compact ? `
            border:none;
            border-radius:4px;
            padding:1px 6px;
            font-size:11px;
            line-height:1.6;
            cursor:pointer;
            flex:0 0 auto;
        ` : `
            border:none;
            border-radius:4px;
            padding:5px 10px;
            cursor:pointer;
            flex:0 0 auto;
        `;

        save.style.cssText = `background:#5865F2; color:white; ${btnStyle}`;
        cancel.style.cssText = `margin-left:5px; background:#ed4245; color:white; ${btnStyle}`;

        const row = document.createElement("div");
        row.style.cssText = `
            margin-top:${compact ? "2px" : "5px"};
            display:flex;
            flex-wrap:wrap;
            align-items:center;
        `;

        row.append(save, cancel);

        const wrapper = document.createElement("div");
        wrapper.style.cssText = `
            display:block;
            width:100%;
            min-width:0;
            max-width:100%;
            box-sizing:border-box;
            position:relative;
        `;

        wrapper.append(textarea, row);

        // --- PREVENT DISCORD REPLY NAV JUMP / FOCUS INTERFERING ---
        // We block these in the bubble phase (false) so that the textarea itself still 
        // receives the events natively (allowing typing, cursor placement, text selection, etc.),
        // while completely preventing Discord's parent containers from detecting them.
        const stopEvents = (e) => {
            e.stopPropagation();
        };

        const eventsToBlock = ["click", "mousedown", "mouseup", "pointerdown", "pointerup", "keydown", "keyup", "keypress"];
        eventsToBlock.forEach(eventName => {
            wrapper.addEventListener(eventName, stopEvents, false);
        });

        content.replaceWith(wrapper);

        // Compact (reply snippet) editors start at content height and grow
        // with input, instead of carrying the full message editor's fixed
        // min-height which would blow out the narrow reply row.
        if (compact) {
            const fit = () => {
                textarea.style.height = "auto";
                textarea.style.height = Math.min(textarea.scrollHeight, 160) + "px";
            };

            fit();
            textarea.addEventListener("input", fit);
        }

        save.onclick = (e) => {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            
            // Defer replacement so the click/mousedown event safely finishes stopping first
            setTimeout(() => {
                content.textContent = textarea.value;
                restoreAncestors();
                wrapper.replaceWith(content);
            }, 0);
        };

        cancel.onclick = (e) => {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            
            // Defer replacement so the click/mousedown event safely finishes stopping first
            setTimeout(() => {
                restoreAncestors();
                wrapper.replaceWith(content);
            }, 0);
        };

        textarea.focus();
    }

    function makeEditIcon(icon, color, title, rightOffset) {
        const button = document.createElement("button");

        button.className = EDIT_BUTTON;
        button.textContent = icon;
        button.title = title;

        button.style.cssText = `
            position:absolute;
            top:3px;
            right:${rightOffset};
            width:18px;
            height:18px;
            display:flex;
            align-items:center;
            justify-content:center;
            padding:0;
            border:none;
            border-radius:4px;
            background:${color};
            color:white;
            font-size:12px;
            cursor:pointer;
            opacity:.5;
            z-index:20;
        `;

        return button;
    }

    function addEditButton(message) {
        if (message.dataset.localEditInjected)
            return;

        const container = message.querySelector(".message__5126c");

        // A reply renders the quoted snippet ABOVE the real message, and
        // both happen to use an id starting with "message-content-" (the
        // snippet uses the id of the message being replied to). Grabbing
        // the first match — like a plain querySelector would — picks the
        // quoted snippet instead of the actual message. The real message
        // is always the LAST one in DOM order.
        const contents = [...message.querySelectorAll('[id^="message-content-"]')];

        if (!container || contents.length === 0)
            return;

        message.dataset.localEditInjected = "true";
        container.style.position = "relative";

        const mainContent = contents[contents.length - 1];
        const repliedContent = contents.length > 1 ? contents[0] : null;

        const editButton = makeEditIcon(
            "✎",
            "#5865F2",
            "Edit locally",
            "3px"
        );

        editButton.onclick = (e) => {
            e.preventDefault();
            e.stopPropagation();
            // Defer showing the edit box to let the button click event finish bubbling away
            setTimeout(() => {
                startEdit(mainContent, false);
            }, 0);
        };

        container.appendChild(editButton);

        // Only present when this message is a reply: a second, separate
        // button that edits the quoted snippet instead of the real message.
        if (repliedContent) {
            const replyEditButton = makeEditIcon(
                "↩",
                "#3ba55c",
                "Edit replied message",
                "23px"
            );

            replyEditButton.onclick = (e) => {
                e.preventDefault();
                e.stopPropagation();
                // Defer showing the edit box to let the button click event finish bubbling away
                setTimeout(() => {
                    startEdit(repliedContent, true);
                }, 0);
            };

            container.appendChild(replyEditButton);
        }
    }

    // --- REMAINDER OF UTILITIES & OBSERVERS STAY UNCHANGED ---
    function scanMessages() {
        document.querySelectorAll('li[id^="chat-messages-"]').forEach(addEditButton);
    }

    function applyGlobalCSS() {
        let styleEl = document.getElementById(STYLE_ID);
        if (!styleEl) {
            styleEl = document.createElement("style");
            styleEl.id = STYLE_ID;
            document.head.appendChild(styleEl);
        }

        styleEl.textContent = editButtonsHidden ? `.${EDIT_BUTTON} { display: none !important; }` : '';
    }

    function clearGlobalCSS() {
        const styleEl = document.getElementById(STYLE_ID);
        if (styleEl) {
            styleEl.remove();
        }
    }

    function createControls() {
        const existing = document.querySelector("." + CONTROL_BOX);
        if (existing) return;

        // Targets the root wrapper holding the direct home element and guild container list
        const treeRoot = document.querySelector('nav[class*="guilds_"] [class*="tree_"]');
        if (!treeRoot) return;

        const box = document.createElement("div");
        box.className = CONTROL_BOX;

        // Clean layout styles: centered, slight margin bottom to create real space above home button
        box.style.cssText = `
            display: flex;
            flex-direction: row;
            align-items: center;
            justify-content: center;
            gap: 5px;
            width: 100%;
            padding: 8px 0 4px 0;
            z-index: 9999;
            box-sizing: border-box;
        `;

        function makeButton(icon, color, title) {
            const b = document.createElement("button");
            b.textContent = icon;
            b.title = title;
            b.style.cssText = `
                width: 16px;
                height: 16px;
                display: flex;
                align-items: center;
                justify-content: center;
                border: none;
                border-radius: 50%;
                background: ${color};
                color: white;
                font-size: 9px;
                line-height: 1;
                cursor: pointer;
                box-shadow: 0 1px 3px rgba(0,0,0,.4);
                flex: 0 0 auto;
            `;
            return b;
        }

        const reset = makeButton("🔄", "#5865F2", "Reset edits");
        reset.onclick = () => {
            document.querySelectorAll("[data-original-text]").forEach(el => {
                el.textContent = el.dataset.originalText;
                delete el.dataset.originalText;
            });
        };

        const toggleVisibility = makeButton(
            editButtonsHidden ? "👁️" : "😑",
            "#23a55a",
            "Toggle edit buttons visibility"
        );

        toggleVisibility.onclick = () => {
            editButtonsHidden = !editButtonsHidden;
            toggleVisibility.textContent = editButtonsHidden ? "👁️" : "😑";
            applyGlobalCSS();
        };

        const remove = makeButton("✖", "#ed4245", "Remove editor");
        remove.onclick = () => {
            document.querySelectorAll("." + EDIT_BUTTON).forEach(e => e.remove());
            document.querySelectorAll('li[id^="chat-messages-"]').forEach(e => delete e.dataset.localEditInjected);
            if (observer) observer.disconnect();
            clearGlobalCSS();
            box.remove();
        };

        box.append(reset, toggleVisibility, remove);

        // Prepend directly into the structural root layout panel to natively offset everything below it
        treeRoot.insertBefore(box, treeRoot.firstChild);
        applyGlobalCSS();
    }

    scanMessages();
    createControls();

    observer = new MutationObserver(() => {
        scanMessages();
        createControls();
    });

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

    console.log("Local editor loaded");

})();