🔨 | Robust + menu injection with MutationObserver and multiple fallback selectors
This commit is contained in:
@@ -655,52 +655,104 @@ function createMenuItem(label, iconSvg, onClick) {
|
||||
return el;
|
||||
}
|
||||
|
||||
// ── Inject items into the + menu popout ─────────────────────────
|
||||
function injectPlusMenuItems() {
|
||||
// Look for open menu scrollers — the + popout is a menu layer
|
||||
const scrollers = document.querySelectorAll('[class*="menu"] [class*="scroller"]');
|
||||
for (const scroller of scrollers) {
|
||||
// Already injected?
|
||||
if (scroller.querySelector("[data-unsloth-menu-item]")) continue;
|
||||
|
||||
// Identify the + menu: it contains items for upload / create thread etc.
|
||||
// We look for any element that hints this is the channel-attach popout.
|
||||
const hint =
|
||||
scroller.querySelector('[class*="upload"], [class*="attach"]') ||
|
||||
scroller.closest('[class*="channelTextArea"]');
|
||||
if (!hint) continue;
|
||||
|
||||
// Append a divider then our two items
|
||||
const divider = document.createElement("div");
|
||||
divider.style.cssText =
|
||||
"margin:4px 6px;border-top:1px solid var(--background-modifier-accent);";
|
||||
|
||||
const askAi = createMenuItem("Ask AI", SPARKLE_ICON, () => {
|
||||
openModal((p) => <PromptModal close={p.close} />);
|
||||
});
|
||||
|
||||
const suggest = createMenuItem("Suggest reply", REPLY_ICON, () => {
|
||||
suggestReply();
|
||||
});
|
||||
|
||||
scroller.appendChild(divider);
|
||||
scroller.appendChild(askAi);
|
||||
scroller.appendChild(suggest);
|
||||
// ── Identify the + popout and inject items ──────────────────────
|
||||
function isAttachMenu(scroller) {
|
||||
// Check menu item text for keywords unique to the + popout
|
||||
const items = scroller.querySelectorAll('[role="menuitem"], [class*="item"]');
|
||||
for (const item of items) {
|
||||
const t = (item.textContent || "").toLowerCase();
|
||||
if (/upload|file|thread|create|attach|app|integration|gif|sticker|poll/i.test(t)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: is the popout positioned near the chat input area?
|
||||
const menuRoot = scroller.closest('[class*="menu"]') || scroller.closest('[class*="popout"]');
|
||||
if (menuRoot) {
|
||||
const cta = document.querySelector('[class*="channelTextArea"]');
|
||||
if (cta) {
|
||||
const mr = menuRoot.getBoundingClientRect();
|
||||
const cr = cta.getBoundingClientRect();
|
||||
if (mr.bottom >= cr.top - 20 && mr.top <= cr.bottom + 20) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Hook the + button to inject items when popout opens ──────────
|
||||
function addItemsToScroller(scroller) {
|
||||
if (!scroller || scroller.querySelector("[data-unsloth-menu-item]")) return;
|
||||
|
||||
const divider = document.createElement("div");
|
||||
divider.style.cssText =
|
||||
"margin:4px 6px;border-top:1px solid var(--background-modifier-accent);";
|
||||
|
||||
const askAi = createMenuItem("Ask AI", SPARKLE_ICON, () => {
|
||||
openModal((p) => <PromptModal close={p.close} />);
|
||||
});
|
||||
|
||||
const suggest = createMenuItem("Suggest reply", REPLY_ICON, () => {
|
||||
suggestReply();
|
||||
});
|
||||
|
||||
scroller.appendChild(divider);
|
||||
scroller.appendChild(askAi);
|
||||
scroller.appendChild(suggest);
|
||||
}
|
||||
|
||||
// ── Strategy 1: MutationObserver watches for any menu popout ────
|
||||
const menuObserver = new MutationObserver((mutations) => {
|
||||
for (const mut of mutations) {
|
||||
for (const node of mut.addedNodes) {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
||||
// The added node may be the menu itself …
|
||||
if (node.matches && node.matches('[class*="menu"]')) {
|
||||
const sc = node.querySelector('[class*="scroller"]');
|
||||
if (sc && isAttachMenu(sc)) addItemsToScroller(sc);
|
||||
}
|
||||
// … or it may contain a menu subtree deeper inside (portals)
|
||||
if (node.querySelectorAll) {
|
||||
node.querySelectorAll('[class*="menu"]').forEach(menu => {
|
||||
const sc = menu.querySelector('[class*="scroller"]');
|
||||
if (sc && isAttachMenu(sc)) addItemsToScroller(sc);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
menuObserver.observe(document.body, { childList: true, subtree: true });
|
||||
scoped.onDispose(() => menuObserver.disconnect());
|
||||
|
||||
// ── Strategy 2: hook the + button click for faster injection ────
|
||||
function hookAttachButton(btn) {
|
||||
if (btn.dataset.unslothHooked) return;
|
||||
btn.dataset.unslothHooked = "1";
|
||||
btn.addEventListener("click", () => {
|
||||
// The popout renders asynchronously, so wait a tick
|
||||
setTimeout(injectPlusMenuItems, 50);
|
||||
let tries = 0;
|
||||
const poll = () => {
|
||||
if (tries++ > 15) return;
|
||||
const scrollers = document.querySelectorAll('[class*="menu"] [class*="scroller"]');
|
||||
for (const sc of scrollers) {
|
||||
if (!sc.querySelector("[data-unsloth-menu-item]") && isAttachMenu(sc)) {
|
||||
addItemsToScroller(sc);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setTimeout(poll, 40);
|
||||
};
|
||||
setTimeout(poll, 30);
|
||||
});
|
||||
}
|
||||
|
||||
// Multiple possible selectors for the + button (Discord changes these)
|
||||
scoped.observeDom(
|
||||
'[class*="channelTextArea"] [class*="attachButton"]',
|
||||
[
|
||||
'[class*="channelTextArea"] [class*="attachButton"]',
|
||||
'[class*="channelTextArea"] [class*="attachWrapper"] > button',
|
||||
'[class*="channelTextArea"] button[class*="attach"]',
|
||||
'[class*="channelTextArea"] [aria-label*="ttach"]',
|
||||
'[class*="channelTextArea"] [class*="uploadInput"]',
|
||||
].join(", "),
|
||||
hookAttachButton,
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user