Same code(from function to end) can also work on Discord Desktop if you open devtools and run it on Console
// ==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");
})();