// ==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);
})();