Created
November 22, 2025 21:10
-
-
Save minanagehsalalma/f363b763ca93f14759029eb2d222659b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Title: Messenger Multi-Forward Tool (WhatsApp Style - Fixed Order) | |
| // Description: Adds visual checkboxes inside message bubbles and fixes forward stacking order. | |
| (function() { | |
| // --- Configuration --- | |
| // We target the specific message container to place checkbox nicely | |
| // "div[role='row']" is the wrapper. Inside we usually find the content. | |
| const SELECTOR_MESSAGE_ROW = '[role="row"]'; | |
| // SVG Path for the "Three Dots" icon (Vertical & Horizontal) | |
| const ICON_PATH_MORE_VERTICAL = "M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"; | |
| const ICON_PATH_MORE_HORIZONTAL = "M3 12c0-1.1.9-2 2-2s2 .9 2 2-.9 2-2 2-2-.9-2-2zm9 2c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm7 0c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"; | |
| let selectionMode = false; | |
| let selectedMessages = new Set(); | |
| // --- UI Styles --- | |
| const styleId = 'messenger-multi-fwd-style'; | |
| if (!document.getElementById(styleId)) { | |
| const style = document.createElement('style'); | |
| style.id = styleId; | |
| style.innerHTML = ` | |
| /* The Checkbox */ | |
| .msg-checkbox { | |
| position: absolute; | |
| top: 50%; | |
| left: 10px; /* Fixed to left of the row */ | |
| transform: translateY(-50%); | |
| width: 28px; | |
| height: 28px; | |
| border-radius: 50%; | |
| border: 2px solid #ccc; | |
| background: rgba(255, 255, 255, 0.9); | |
| z-index: 9999; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.2); | |
| } | |
| .msg-checkbox.checked { | |
| background: #00a884; | |
| border-color: #00a884; | |
| } | |
| .msg-checkbox.checked::after { | |
| content: '✓'; | |
| color: white; | |
| font-weight: bold; | |
| font-size: 18px; | |
| } | |
| /* Highlight the entire row when selected */ | |
| .msg-row-selected { | |
| background-color: rgba(0, 168, 132, 0.08) !important; | |
| position: relative !important; /* Ensure absolute child works */ | |
| } | |
| /* Add a border to the inner bubble if possible (visual helper) */ | |
| .msg-row-selected div[role="none"] { | |
| /* specific targeting might be needed here depending on theme */ | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| } | |
| // --- Control Panel --- | |
| const panel = document.createElement('div'); | |
| panel.style.cssText = ` | |
| position: fixed; | |
| top: 10px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| z-index: 10000; | |
| background: #f0f2f5; | |
| padding: 10px 20px; | |
| border-radius: 24px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.3); | |
| display: flex; | |
| gap: 15px; | |
| font-family: sans-serif; | |
| align-items: center; | |
| border: 1px solid #ddd; | |
| `; | |
| const toggleBtn = document.createElement('button'); | |
| toggleBtn.innerHTML = "<b>Select Messages</b>"; | |
| toggleBtn.style.cssText = "padding: 8px 16px; background: white; color: black; border: 1px solid #ccc; border-radius: 18px; cursor: pointer; font-size: 14px;"; | |
| const actionBtn = document.createElement('button'); | |
| actionBtn.innerText = "Forward (0)"; | |
| actionBtn.style.cssText = "padding: 8px 16px; background: #00a884; color: white; border: none; border-radius: 18px; cursor: not-allowed; opacity: 0.6; font-weight: bold;"; | |
| actionBtn.disabled = true; | |
| const statusText = document.createElement('span'); | |
| statusText.style.fontSize = '12px'; | |
| statusText.style.color = '#555'; | |
| statusText.innerText = "Idle"; | |
| panel.appendChild(toggleBtn); | |
| panel.appendChild(actionBtn); | |
| panel.appendChild(statusText); | |
| document.body.appendChild(panel); | |
| // --- Logic --- | |
| toggleBtn.onclick = () => { | |
| selectionMode = !selectionMode; | |
| toggleBtn.style.background = selectionMode ? "#dcf8c6" : "white"; | |
| toggleBtn.innerHTML = selectionMode ? "<b>Cancel Selection</b>" : "<b>Select Messages</b>"; | |
| if (!selectionMode) { | |
| clearSelections(); | |
| statusText.innerText = "Idle"; | |
| } else { | |
| statusText.innerText = "Click messages to select"; | |
| } | |
| }; | |
| // Global click listener for selection | |
| document.addEventListener('click', (e) => { | |
| if (!selectionMode) return; | |
| // Find the Row | |
| const msgRow = e.target.closest(SELECTOR_MESSAGE_ROW); | |
| // Prevent selecting the control panel itself | |
| if (panel.contains(e.target)) return; | |
| if (msgRow) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| toggleSelection(msgRow); | |
| } | |
| }, true); | |
| function toggleSelection(row) { | |
| if (selectedMessages.has(row)) { | |
| selectedMessages.delete(row); | |
| row.classList.remove('msg-row-selected'); | |
| const chk = row.querySelector('.msg-checkbox'); | |
| if (chk) chk.remove(); | |
| } else { | |
| selectedMessages.add(row); | |
| row.classList.add('msg-row-selected'); | |
| // Create Visual Checkbox | |
| const chk = document.createElement('div'); | |
| chk.className = 'msg-checkbox checked'; | |
| // Append to row (Row is usually full width) | |
| row.style.position = 'relative'; | |
| row.appendChild(chk); | |
| } | |
| updateUI(); | |
| } | |
| function updateUI() { | |
| actionBtn.innerText = `Forward (${selectedMessages.size})`; | |
| if (selectedMessages.size > 0) { | |
| actionBtn.style.opacity = '1'; | |
| actionBtn.style.cursor = 'pointer'; | |
| actionBtn.disabled = false; | |
| statusText.innerText = `${selectedMessages.size} selected`; | |
| } else { | |
| actionBtn.style.opacity = '0.6'; | |
| actionBtn.style.cursor = 'not-allowed'; | |
| actionBtn.disabled = true; | |
| statusText.innerText = "Click messages to select"; | |
| } | |
| } | |
| function clearSelections() { | |
| selectedMessages.forEach(row => { | |
| row.classList.remove('msg-row-selected'); | |
| const chk = row.querySelector('.msg-checkbox'); | |
| if (chk) chk.remove(); | |
| }); | |
| selectedMessages.clear(); | |
| updateUI(); | |
| } | |
| // --- Execution Helpers --- | |
| function triggerHover(element) { | |
| const event = new MouseEvent('mouseover', { | |
| view: window, | |
| bubbles: true, | |
| cancelable: true | |
| }); | |
| element.dispatchEvent(event); | |
| } | |
| function findThreeDotsButton(container) { | |
| // 1. SVG Search (Most Robust) | |
| const paths = container.querySelectorAll('path'); | |
| for (let p of paths) { | |
| const d = p.getAttribute('d'); | |
| if (d === ICON_PATH_MORE_VERTICAL || d === ICON_PATH_MORE_HORIZONTAL) { | |
| return p.closest('[role="button"]') || p.closest('div[aria-label]'); | |
| } | |
| } | |
| // 2. Aria Label Fallback | |
| return container.querySelector('[aria-label*="More"], [aria-label*="actions"], [aria-label="Menu"]'); | |
| } | |
| // --- Main Forwarding Logic --- | |
| actionBtn.onclick = async () => { | |
| if (selectedMessages.size === 0) return; | |
| // 1. Sort Messages Top-to-Bottom (Chronological) | |
| let messages = Array.from(selectedMessages).sort((a, b) => { | |
| return (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) ? -1 : 1; | |
| }); | |
| // 2. REVERSE the array for processing | |
| // Reason: We open popups one by one. The last popup opened ends up on TOP. | |
| // We want the FIRST message (Chronologically) to be on TOP so the user sends it first. | |
| // So we open: Message 3 (Bottom layer) -> Message 2 -> Message 1 (Top layer). | |
| messages = messages.reverse(); | |
| selectionMode = false; | |
| toggleBtn.innerText = "Processing..."; | |
| for (let i = 0; i < messages.length; i++) { | |
| const msg = messages[i]; | |
| statusText.innerText = `Opening ${i+1}/${messages.length}...`; | |
| try { | |
| // Scroll & Hover | |
| msg.scrollIntoView({ behavior: 'auto', block: 'center' }); | |
| triggerHover(msg); | |
| await new Promise(r => setTimeout(r, 300)); | |
| const moreBtn = findThreeDotsButton(msg); | |
| if (!moreBtn) { | |
| console.warn("Menu button not found, skipping."); | |
| continue; | |
| } | |
| moreBtn.click(); | |
| await new Promise(r => setTimeout(r, 800)); // Wait for menu animation | |
| // Find "Forward" in the opened menu | |
| // We look for the menu that is currently visible/highest z-index | |
| const menuItems = document.querySelectorAll('[role="menuitem"]'); | |
| let forwardBtn = null; | |
| for (let item of menuItems) { | |
| if (item.innerText.includes('Forward') || item.querySelector('[aria-label="Forward"]')) { | |
| // Check if this menu item corresponds to the currently open menu (usually implies visibility) | |
| if (item.offsetParent !== null) { | |
| forwardBtn = item; | |
| break; | |
| } | |
| } | |
| } | |
| if (forwardBtn) { | |
| forwardBtn.click(); | |
| console.log(`Forward opened for msg ${i}`); | |
| // Increase delay to ensure previous modal renders before next one stacks | |
| await new Promise(r => setTimeout(r, 1200)); | |
| } else { | |
| // Close menu if forward not found | |
| document.body.click(); | |
| } | |
| } catch (e) { | |
| console.error(e); | |
| } | |
| } | |
| statusText.innerText = "Done!"; | |
| alert("Processing Complete!\n\nThe Forward windows are now stacked.\n\nThe TOP window corresponds to your FIRST selected message.\nSend them one by one."); | |
| clearSelections(); | |
| toggleBtn.innerHTML = "<b>Select Messages</b>"; | |
| }; | |
| console.log("Messenger Tool Loaded: Fixed Order & Visibility."); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment