// ==UserScript== // @name Mastodon Thread-Tree Sidebar // @namespace https://nobsagile.github.io/mastodon-thread/ // @version 1.6 // @description Adds a button to every post in the Mastodon web UI that opens the whole thread as a readable reply tree in a side drawer. // @description:de Fügt jedem Post in der Mastodon-Weboberfläche einen Button hinzu, der den kompletten Thread als lesbaren Antwort-Baum in einer Sidebar öffnet. // @author nobsagile // @homepageURL https://github.com/nobsagile/mastodon-thread // @supportURL https://github.com/nobsagile/mastodon-thread/issues // @downloadURL https://raw.githubusercontent.com/nobsagile/mastodon-thread/main/mastodon-thread-sidebar.user.js // @updateURL https://raw.githubusercontent.com/nobsagile/mastodon-thread/main/mastodon-thread-sidebar.user.js // @match *://*/* // @grant none // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; // Nur auf Mastodon-Weboberflächen aktiv werden (React-Mountnode #mastodon). // Dadurch kann @match *://*/* alle Instanzen abdecken, ohne anderswo zu laufen. if (!document.getElementById('mastodon')) return; const VIEWER = 'https://nobsagile.github.io/mastodon-thread/?url='; const PANEL_WIDTH = '50vw'; // halbe Fensterbreite // Icon (Lucide "list-tree") – passt sich per currentColor der Post-Farbe an const ICON_SVG = '' + '' + ''; // ---- Sidebar (iframe) einmalig bauen ------------------------------------- let panel, iframe; function buildPanel() { if (panel) return; panel = document.createElement('div'); panel.id = 'mt-sidebar'; Object.assign(panel.style, { position: 'fixed', top: '0', right: '0', height: '100vh', width: PANEL_WIDTH, maxWidth: '95vw', background: '#191b22', boxShadow: '-2px 0 12px rgba(0,0,0,.5)', zIndex: '99999', transform: 'translateX(100%)', transition: 'transform .2s ease', display: 'flex', flexDirection: 'column', }); const bar = document.createElement('div'); Object.assign(bar.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 12px', background: '#282c37', color: '#fff', font: '600 14px/1.2 system-ui, sans-serif', flex: '0 0 auto', }); bar.innerHTML = '🐘 Thread'; const close = document.createElement('button'); close.textContent = '✕'; Object.assign(close.style, { background: 'transparent', border: 'none', color: '#fff', fontSize: '18px', cursor: 'pointer', lineHeight: '1', }); close.addEventListener('click', hidePanel); bar.appendChild(close); iframe = document.createElement('iframe'); Object.assign(iframe.style, { flex: '1 1 auto', width: '100%', border: 'none', }); panel.appendChild(bar); panel.appendChild(iframe); document.body.appendChild(panel); // ESC schließt document.addEventListener('keydown', (e) => { if (e.key === 'Escape') hidePanel(); }); } function showPanel(url) { buildPanel(); iframe.src = VIEWER + encodeURIComponent(url); requestAnimationFrame(() => (panel.style.transform = 'translateX(0)')); } function hidePanel() { if (panel) panel.style.transform = 'translateX(100%)'; } function absolute(href) { try { return new URL(href, location.origin).href; } catch { return href; } } // ---- Button in die Action-Bar jedes Posts einklinken --------------------- // Der Zeitstempel-Link (.status__relative-time) desselben Posts trägt die // kanonische Post-URL (bei Remote-Posts sogar die Original-Instanz). function injectBar(bar) { if (bar.dataset.mtInjected) return; const status = bar.closest('.status, .detailed-status') || bar.parentElement; const link = status && status.querySelector('a.status__relative-time, a.detailed-status__datetime'); if (!link) return; const url = absolute(link.href); if (!url) return; bar.dataset.mtInjected = '1'; const btn = document.createElement('button'); btn.type = 'button'; btn.title = 'Thread als Baum anzeigen'; btn.setAttribute('aria-label', 'Thread als Baum anzeigen'); btn.innerHTML = ICON_SVG; // Optik an die vorhandenen Action-Bar-Buttons angleichen const sample = bar.querySelector('button'); if (sample) { btn.className = sample.className; } else { Object.assign(btn.style, { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', background: 'transparent', border: 'none', cursor: 'pointer', color: 'currentColor', }); } btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); showPanel(url); }); bar.appendChild(btn); } function scan(root) { if (!root.querySelectorAll) return 0; const bars = root.querySelectorAll( '.status__action-bar, .detailed-status__action-bar' ); bars.forEach(injectBar); return bars.length; } // ---- MutationObserver: Mastodon rendert Posts dynamisch nach ------------- const observer = new MutationObserver((muts) => { for (const m of muts) { for (const node of m.addedNodes) { if (node.nodeType === 1) scan(node); } } }); observer.observe(document.body, { childList: true, subtree: true }); scan(document); })();