On X Quote Posts pages, show inner Quote content once at top, then all Quote Posts as Replies. Waits for image loads, ensures posts remain clickable.
// ==UserScript==
// @name X Quotes Cleaner
// @namespace https://x.com/quote-cleaner
// @version 6.0
// @description On X Quote Posts pages, show inner Quote content once at top, then all Quote Posts as Replies. Waits for image loads, ensures posts remain clickable.
// @author Grok (complained at by nanimonull)
// @match https://x.com/*
// @match https://twitter.com/*
// @grant none
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
'use strict';
let originalMoved = false;
let topPostObserver = null;
let currentUrl = '';
let lastUrl = window.location.href;
// @match /quotes* does not work because X URL navigation changes are sneaky (unless user fully reloads page)
// So @match ALL X urls, but only do anything if URL is actually /quotes
function shouldRun() {
return window.location.pathname.includes('/status/') &&
window.location.pathname.includes('/quotes');
}
function checkUrlChange() {
const currentUrl = window.location.href;
if (currentUrl === lastUrl) return;
lastUrl = currentUrl;
console.log('[Quotes Cleaner] URL changed →', currentUrl);
const isQuotes = shouldRun();
if (!isQuotes) {
originalMoved = false;
if (topPostObserver) {
topPostObserver.disconnect();
topPostObserver = null;
}
return;
}
// We are on a quotes page
originalMoved = false;
setTimeout(processQuotes, 600);
}
// === Main hooks ===
setInterval(checkUrlChange, 400);
window.addEventListener('popstate', checkUrlChange);
window.addEventListener('pushstate', checkUrlChange);
function logImages(innerBlock) {
console.log('[Quotes Cleaner Image Debug] === Images in inner quoted block ===');
const pfp = innerBlock.querySelector('img[src*="profile_images"]');
console.log('[Quotes Cleaner Image Debug] PFP:', pfp ? pfp.src : 'NONE');
const photos = innerBlock.querySelectorAll('img[src*="media/"]');
photos.forEach((img, i) => {
console.log(`[Quotes Cleaner Image Debug] Photo ${i+1}:`, img.src || 'NONE');
});
return !!(pfp && pfp.src) || photos.length > 0;
}
function processQuotes() {
if (!shouldRun() || originalMoved) return;
const timeline = document.querySelector('div[aria-label="Timeline: Search timeline"]');
if (!timeline) return;
const articles = Array.from(timeline.querySelectorAll('article[data-testid="tweet"]'));
if (articles.length < 2) return;
console.log(`[Quotes Cleaner Debug] Found ${articles.length} articles`);
const firstArticle = articles[0];
let innerBlock = firstArticle.querySelector('div.r-adacv.r-1udh08x.r-1kqtdi0.r-1867qdf') ||
firstArticle.querySelector('div[role="link"]');
if (innerBlock) {
// Always returns true thanks to Profile Pictures on posts
const hasImages = logImages(innerBlock);
if (hasImages) {
// Derive original post URL from current quotes page URL
let originalUrl = window.location.href
.replace(/\/quotes.*$/, '')
.split('?')[0];
const clonedInner = innerBlock.cloneNode(true);
const wrapper = document.createElement('div');
wrapper.dataset.testid = 'cellInnerDiv';
const a = document.createElement('a');
a.href = originalUrl;
a.style.cssText = 'text-decoration:none;color:inherit;display:block;';
a.appendChild(clonedInner);
wrapper.appendChild(a);
timeline.prepend(wrapper);
// Start watching this wrapper in case X unloads it later
setupTopPostWatcher(wrapper);
// Remove inner block from original first post
innerBlock.remove();
// Remove hanging "Quote" label safely after move
setTimeout(() => {
const hanging = firstArticle.querySelector('div.r-9aw3ui.r-1s2bzr4');
if (hanging) hanging.remove();
}, 500);
originalMoved = true;
console.log('[Quotes Cleaner] ✅ SUCCESS: Inner quoted content moved to top - images preserved');
} else {
console.log('[Quotes Cleaner Debug] Inner block found but images not ready yet - waiting...');
}
}
}
// Clean others - also guarded
function cleanOthers() {
if (!shouldRun() || !originalMoved) return;
const articles = document.querySelectorAll('article[data-testid="tweet"]');
let cleaned = 0;
articles.forEach((article, i) => {
if (i === 0) return;
const blocks = article.querySelectorAll('div.r-adacv, div[role="link"], div.r-1s2bzr4, div.r-9aw3ui');
blocks.forEach(block => {
if (block.textContent && block.textContent.length > 20) {
block.remove();
cleaned++;
}
});
});
if (cleaned > 0) console.log(`[Quotes Cleaner] Cleaned ${cleaned} inner blocks`);
}
// Watch if our moved top post gets unloaded by X's virtualization
function setupTopPostWatcher(wrapper) {
if (topPostObserver) topPostObserver.disconnect();
topPostObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.removedNodes.length > 0) {
const stillExists = document.contains(wrapper);
if (!stillExists) {
console.log('[Quotes Cleaner] Top post was unloaded by X → resetting flag');
originalMoved = false;
topPostObserver.disconnect();
topPostObserver = null;
break;
}
}
}
});
const timeline = document.querySelector('div[aria-label="Timeline: Search timeline"]');
if (timeline) {
topPostObserver.observe(timeline, { childList: true, subtree: true });
}
}
setInterval(processQuotes, 300);
setInterval(cleanOthers, 500);
// Back button + cleanup
window.addEventListener('popstate', () => {
originalMoved = false;
if (topPostObserver) {
topPostObserver.disconnect();
topPostObserver = null;
}
});
console.log('X Quotes Cleaner v6.0 ready');
})();