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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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");

})();