Skip to content

Instantly share code, notes, and snippets.

@minanagehsalalma
Created November 22, 2025 21:10
Show Gist options
  • Select an option

  • Save minanagehsalalma/f363b763ca93f14759029eb2d222659b to your computer and use it in GitHub Desktop.

Select an option

Save minanagehsalalma/f363b763ca93f14759029eb2d222659b to your computer and use it in GitHub Desktop.
// 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