diff --git a/plugins/unsloth-chat/index.jsx b/plugins/unsloth-chat/index.jsx
index 1437a82..4ca7812 100644
--- a/plugins/unsloth-chat/index.jsx
+++ b/plugins/unsloth-chat/index.jsx
@@ -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) => );
- });
-
- 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) => );
+ });
+
+ 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,
);