// ==UserScript== // @name Twitter/X Layout Modifier // @namespace http://tampermonkey.net/ // @license MIT // @version 1.4.1 // @description Remove right sidebar, expand middle column, and inject custom vertical image stack with sizing fixes // @author maye9999 // @match https://twitter.com/* // @match https://x.com/* // @grant none // @downloadURL none // ==/UserScript== (function () { 'use strict'; // 1. CSS for Structural Layout Changes (Sidebar & Column Width) const style = document.createElement('style'); style.innerHTML = ` /* Remove the right sidebar */ [data-testid="sidebarColumn"] { display: none !important; } /* Expand the middle column (primaryColumn) */ [data-testid="primaryColumn"] { max-width: 100% !important; width: 100% !important; flex-basis: auto !important; } /* Ensure parent containers allow expansion */ div:has(> [data-testid="primaryColumn"]) { max-width: 100% !important; width: 100% !important; } .r-1ye8kvj { max-width: 100% !important; } /* Custom class for our injected container */ .ag-custom-media-stack { display: flex; flex-direction: row; /* Allow horizontal flow */ flex-wrap: wrap; /* Allow wrapping */ width: 100%; margin-top: 10px; gap: 12px; /* Space between images (both horizontal and vertical) */ align-items: flex-start; } /* Image styling */ .ag-custom-media-stack img { width: auto; max-width: 100%; /* [EDIT HERE] Change to eg. '800px' or '80%' to limit width on laptops */ max-height: 85vh; /* [EDIT HERE] Change to eg. '60vh' to make images shorter/fit screen better */ height: auto; border-radius: 12px; display: block; border: 1px solid rgba(255,255,255,0.1); cursor: pointer; object-fit: contain; flex: 0 1 auto; /* Allow image to be its natural size, but shrink if needed */ } /* Lightbox-like effect on click (optional simple zoom) */ .ag-custom-media-stack img:active { transform: scale(1.02); transition: transform 0.1s; } `; document.head.appendChild(style); // 2. JavaScript for Custom Layout Injection function processTweets() { // Find all tweets or photo-containing elements const photos = document.querySelectorAll('[data-testid="tweetPhoto"]'); // Map to store unique root containers we've found in this pass const rootsToProcess = new Map(); // Map photos.forEach(photo => { if (photo.hasAttribute('data-ag-processed')) return; // Extract URL const img = photo.querySelector('img'); if (!img) return; let src = img.src; if (src.includes('&name=')) { src = src.replace(/&name=[a-z0-9]+/, '&name=large'); } // Find root container logic const tweet = photo.closest('[data-testid="tweet"]'); if (tweet) { // Inside this tweet, find the container that holds ALL images. const allImagesInTweet = Array.from(tweet.querySelectorAll('[data-testid="tweetPhoto"]')); if (allImagesInTweet.length > 0) { // Find common ancestor let ancestor = allImagesInTweet[0]; if (allImagesInTweet.length > 1) { // Simple common ancestor algorithm const parents = new Set(); let p = ancestor; while (p && p !== tweet) { parents.add(p); p = p.parentElement; } // Check second image parents against set let p2 = allImagesInTweet[1].parentElement; while (p2 && p2 !== tweet) { if (parents.has(p2)) { ancestor = p2; break; } p2 = p2.parentElement; } } // Climb up from ancestor until the parent contains tweetText (sibling logic) let contentRoot = ancestor; while (contentRoot.parentElement && contentRoot.parentElement !== tweet) { if (contentRoot.parentElement.querySelector('[data-testid="tweetText"]')) { break; } contentRoot = contentRoot.parentElement; } if (!rootsToProcess.has(contentRoot)) { rootsToProcess.set(contentRoot, []); } // Check if we already have this URL for this root const list = rootsToProcess.get(contentRoot); if (!list.includes(src)) { list.push(src); } // Mark photo as pending processing photo.setAttribute('data-ag-processed', 'true'); } } }); // Now process the roots rootsToProcess.forEach((urls, root) => { // 1. Tag root as processed if (root.getAttribute('data-ag-replaced')) return; // 2. Hide Root root.style.display = "none"; root.setAttribute('data-ag-replaced', 'true'); // 3. Inject Custom Stack const stack = document.createElement('div'); stack.className = 'ag-custom-media-stack'; urls.forEach((url, index) => { const img = document.createElement('img'); img.src = url; img.onclick = (e) => { e.stopPropagation(); window.open(url, '_blank'); }; stack.appendChild(img); }); // Insert after the hidden root root.insertAdjacentElement('afterend', stack); }); } // 3. Observe the DOM for new tweets const observer = new MutationObserver((mutations) => { processTweets(); }); observer.observe(document.body, { childList: true, subtree: true }); // Initial run setTimeout(processTweets, 500); setInterval(processTweets, 2000); })();