Adds a Milady-eyes button under each tweet that replies "milady"
// ==UserScript==
// @name X Milady Reply Button
// @namespace https://tampermonkey.net/
// @version 0.2
// @description Adds a Milady-eyes button under each tweet that replies "milady"
// @match https://x.com/*
// @match https://twitter.com/*
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const BUTTON_CLASS = 'milady-reply-button';
const REPLY_TEXT = 'milady';
let busy = false;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function createButton() {
const btn = document.createElement('button');
btn.textContent = '👀';
btn.title = 'Reply "milady"';
btn.className = BUTTON_CLASS;
btn.type = 'button';
btn.style.border = 'none';
btn.style.background = 'transparent';
btn.style.cursor = 'pointer';
btn.style.fontSize = '18px';
btn.style.marginLeft = '8px';
btn.style.padding = '0 4px';
return btn;
}
function isVisible(el) {
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
function getReplyDialog() {
const dialogs = [...document.querySelectorAll('[role="dialog"]')]
.filter(isVisible);
return dialogs.find(dialog => {
return dialog.querySelector('[data-testid="tweetButton"]') ||
dialog.querySelector('[data-testid="tweetButtonInline"]');
}) || dialogs[dialogs.length - 1] || null;
}
function getReplyTextboxFromDialog(dialog) {
if (!dialog) return null;
const boxes = [...dialog.querySelectorAll(
'div[role="textbox"][contenteditable="true"]'
)].filter(isVisible);
if (!boxes.length) return null;
return boxes[boxes.length - 1];
}
async function waitForReplyTextbox(timeoutMs = 5000) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const dialog = getReplyDialog();
const textbox = getReplyTextboxFromDialog(dialog);
if (textbox) return textbox;
await sleep(50);
}
return null;
}
function placeCursorAtEnd(el) {
el.click();
el.focus();
const range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
function pasteMilady(textbox) {
placeCursorAtEnd(textbox);
const dataTransfer = new DataTransfer();
dataTransfer.setData('text/plain', REPLY_TEXT);
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dataTransfer
});
textbox.dispatchEvent(pasteEvent);
}
async function replyMilady(article) {
if (busy) return;
busy = true;
try {
const replyButton = article.querySelector('[data-testid="reply"]');
if (!replyButton) return;
replyButton.click();
const textbox = await waitForReplyTextbox();
if (!textbox) return;
await sleep(300);
pasteMilady(textbox);
} finally {
await sleep(600);
busy = false;
}
}
function enhanceTweet(article) {
if (article.querySelector(`.${BUTTON_CLASS}`)) return;
const actionBar = article.querySelector('[role="group"]');
if (!actionBar) return;
const btn = createButton();
btn.addEventListener('click', event => {
event.preventDefault();
event.stopPropagation();
replyMilady(article);
});
actionBar.appendChild(btn);
}
function scan() {
document.querySelectorAll('article[data-testid="tweet"]').forEach(enhanceTweet);
}
scan();
const observer = new MutationObserver(scan);
observer.observe(document.body, {
childList: true,
subtree: true
});
})();