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