Some checks failed
Build & Push Docker Image / build-and-push (push) Failing after 5s
- FastAPI-based Matrix chat export tool with E2E support - Dockerfile with Python 3.12-slim base image - Docker Compose with dedicated network configuration - Gitea Actions workflow for automated image builds - README with setup instructions and feature overview - MIT license Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
267 lines
12 KiB
HTML
267 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
<title>Chat Export</title>
|
|
<style>
|
|
:root {
|
|
--bg:#0b141a;--bg-chat:#0b141a;--msg-own:#005c4b;--msg-other:#202c33;
|
|
--msg-own-h:#006d5a;--msg-other-h:#263845;--text:#e9edef;--text2:#8696a0;
|
|
--border:#222d34;--accent:#00a884;--accent2:#06cf9c;--pill-bg:#182229;
|
|
--header:#202c33;--link:#53bdeb;--shadow:0 1px 2px rgba(0,0,0,.3);
|
|
--sys-bg:#182229;--sys-text:#8696a0;
|
|
}
|
|
@media(prefers-color-scheme:light){:root{
|
|
--bg:#efeae2;--bg-chat:#efeae2;--msg-own:#d9fdd3;--msg-other:#fff;
|
|
--msg-own-h:#c8f0c2;--msg-other-h:#f5f5f5;--text:#111b21;--text2:#667781;
|
|
--border:#e9edef;--accent:#008069;--accent2:#00a884;--pill-bg:#fff;
|
|
--header:#008069;--link:#027eb5;--shadow:0 1px 3px rgba(0,0,0,.12);
|
|
--sys-bg:rgba(255,255,255,.9);--sys-text:#54656f;
|
|
}}
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;
|
|
background:var(--bg);color:var(--text);line-height:1.45;font-size:14.2px}
|
|
::-webkit-scrollbar{width:6px}
|
|
::-webkit-scrollbar-thumb{background:#374045;border-radius:3px}
|
|
.app{max-width:1200px;margin:0 auto;min-height:100vh;display:flex;flex-direction:column;
|
|
background:var(--bg-chat);box-shadow:0 0 60px rgba(0,0,0,.3)}
|
|
.header{background:var(--header);padding:14px 20px;display:flex;align-items:center;
|
|
gap:14px;position:sticky;top:0;z-index:100;border-bottom:1px solid var(--border)}
|
|
.header-avatar{width:42px;height:42px;border-radius:50%;background:var(--accent);
|
|
display:flex;align-items:center;justify-content:center;font-size:20px;color:#fff;flex-shrink:0}
|
|
.header-info h1{font-size:16px;font-weight:600;color:#fff}
|
|
.header-info .sub{font-size:12px;color:rgba(255,255,255,.7);margin-top:1px}
|
|
.chat{flex:1;padding:8px 40px 20px;overflow-y:auto}
|
|
.date-separator{display:flex;justify-content:center;margin:16px 0 8px}
|
|
.date-pill{background:var(--pill-bg);color:var(--text2);padding:5px 12px;
|
|
border-radius:8px;font-size:12.5px;box-shadow:var(--shadow)}
|
|
.msg-group{margin-bottom:2px;display:flex}
|
|
.msg-group.own{justify-content:flex-end}
|
|
.msg-group.other{justify-content:flex-start}
|
|
.msg{max-width:75%;min-width:120px;padding:6px 7px 8px 9px;border-radius:8px;
|
|
position:relative;box-shadow:var(--shadow);word-wrap:break-word;overflow-wrap:break-word}
|
|
.msg-group.own .msg{background:var(--msg-own);border-top-right-radius:0}
|
|
.msg-group.other .msg{background:var(--msg-other);border-top-left-radius:0}
|
|
.msg-group.own .msg:hover{background:var(--msg-own-h)}
|
|
.msg-group.other .msg:hover{background:var(--msg-other-h)}
|
|
.msg .sender{font-size:13px;font-weight:600;margin-bottom:2px;display:block}
|
|
.msg .body{white-space:pre-wrap;font-size:14.2px;line-height:1.45;overflow-x:auto}
|
|
.msg .body pre{background:rgba(0,0,0,.2);padding:8px 10px;border-radius:4px;
|
|
overflow-x:auto;white-space:pre;font-size:13px;line-height:1.4;margin:4px 0}
|
|
.msg .body code{background:rgba(0,0,0,.15);padding:1px 4px;border-radius:3px;font-size:13px}
|
|
.msg .body pre code{background:none;padding:0}
|
|
.msg .body a{color:var(--link);text-decoration:none}
|
|
.msg .body a:hover{text-decoration:underline}
|
|
.msg .meta{display:flex;justify-content:flex-end;gap:4px;margin-top:2px;
|
|
float:right;margin-left:12px;position:relative;top:5px}
|
|
.msg .meta .time{font-size:11px;color:var(--text2)}
|
|
.msg .media-wrap{margin:4px -3px;border-radius:6px;overflow:hidden}
|
|
.msg .media-wrap img{max-width:100%;max-height:400px;display:block;cursor:pointer;
|
|
border-radius:6px;transition:opacity .15s}
|
|
.msg .media-wrap img:hover{opacity:.92}
|
|
.msg .media-wrap video{max-width:100%;max-height:400px;display:block;border-radius:6px;background:#000}
|
|
.msg .media-wrap audio{width:100%;min-width:240px;height:40px;margin:4px 0}
|
|
.msg .file-attach{display:flex;align-items:center;gap:10px;padding:8px 10px;
|
|
background:rgba(0,0,0,.1);border-radius:6px;margin:4px 0;text-decoration:none;
|
|
color:var(--text);transition:background .15s}
|
|
.msg .file-attach:hover{background:rgba(0,0,0,.18)}
|
|
.msg .file-attach .file-icon{font-size:28px;flex-shrink:0}
|
|
.msg .file-attach .file-name{font-size:13.5px;font-weight:500;white-space:nowrap;
|
|
overflow:hidden;text-overflow:ellipsis}
|
|
.msg .file-attach .file-size{font-size:11.5px;color:var(--text2)}
|
|
.system-event{display:flex;justify-content:center;margin:6px 0}
|
|
.system-event span{background:var(--sys-bg);color:var(--sys-text);padding:4px 12px;
|
|
border-radius:6px;font-size:12px;box-shadow:var(--shadow);text-align:center}
|
|
.emote-msg{font-style:italic;color:var(--text2)}
|
|
.redacted,.encrypted-msg{font-style:italic;color:var(--text2);font-size:13px}
|
|
.footer{background:var(--header);padding:12px 20px;text-align:center;
|
|
font-size:12px;color:var(--text2);border-top:1px solid var(--border)}
|
|
.lightbox{display:none;position:fixed;top:0;left:0;width:100%;height:100%;
|
|
background:rgba(0,0,0,.92);z-index:1000;cursor:zoom-out;justify-content:center;align-items:center}
|
|
.lightbox.active{display:flex}
|
|
.lightbox img{max-width:95%;max-height:95%;object-fit:contain;border-radius:4px}
|
|
.scroll-top{position:fixed;bottom:20px;right:20px;width:44px;height:44px;
|
|
border-radius:50%;background:var(--accent);color:#fff;border:none;cursor:pointer;
|
|
display:none;align-items:center;justify-content:center;font-size:20px;
|
|
box-shadow:0 2px 10px rgba(0,0,0,.3);z-index:50}
|
|
.scroll-top.visible{display:flex}
|
|
#loading{display:flex;justify-content:center;align-items:center;padding:40px;color:var(--text2);font-size:15px}
|
|
@media(max-width:600px){.chat{padding:8px 12px 20px}.msg{max-width:92%}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app">
|
|
<header class="header">
|
|
<div class="header-avatar">💬</div>
|
|
<div class="header-info">
|
|
<h1 id="room-name">Lade...</h1>
|
|
<div class="sub" id="room-info"></div>
|
|
</div>
|
|
</header>
|
|
<div class="chat" id="chat">
|
|
<div id="loading">Nachrichten werden geladen...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="lightbox" id="lightbox" onclick="closeLightbox()">
|
|
<img id="lightbox-img" src="" alt="">
|
|
</div>
|
|
<button class="scroll-top" id="scrollTop" title="Nach oben">↑</button>
|
|
<div class="footer" id="footer"></div>
|
|
|
|
<script>
|
|
var D = __DATA__;
|
|
|
|
document.title = "Chat: " + D.roomName;
|
|
document.getElementById("room-name").textContent = D.roomName;
|
|
document.getElementById("room-info").textContent = D.total + " Nachrichten · " + D.firstDate + " bis " + D.lastDate;
|
|
document.getElementById("footer").innerHTML = "Matrix Chat Export · " + D.total + " Nachrichten · " + D.firstDate + " bis " + D.lastDate + " · Erstellt am " + D.exportDate;
|
|
|
|
var CHUNK = 200;
|
|
var rendered = 0;
|
|
var lastSender = "";
|
|
var chat = document.getElementById("chat");
|
|
var msgs = D.messages;
|
|
|
|
document.getElementById("loading").style.display = "none";
|
|
|
|
function esc(s) {
|
|
if (!s) return "";
|
|
var d = document.createElement("div");
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function linkify(t) {
|
|
return t.replace(/(https?:\/\/[^\s<>&]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>');
|
|
}
|
|
|
|
function renderBody(m) {
|
|
var mt = m.mt;
|
|
if (mt === "m.text" || mt === "m.notice") {
|
|
return m.fb ? '<div class="body">' + m.fb + "</div>"
|
|
: '<div class="body">' + linkify(esc(m.b)) + "</div>";
|
|
}
|
|
if (mt === "m.emote") {
|
|
return '<div class="body emote-msg">* ' + esc(m.b) + "</div>";
|
|
}
|
|
if (mt === "m.image") {
|
|
if (m.m) return '<div class="media-wrap"><a href="media/' + m.m + '" target="_blank"><img src="media/' + m.m + '" alt="' + esc(m.b) + '" loading="lazy" onclick="openLightbox(this.src);return false"></a></div>';
|
|
return '<div class="body">[Bild: ' + esc(m.b) + "]</div>";
|
|
}
|
|
if (mt === "m.video") {
|
|
if (m.m) return '<div class="media-wrap"><video controls preload="metadata" src="media/' + m.m + '"></video></div>';
|
|
return '<div class="body">[Video: ' + esc(m.b) + "]</div>";
|
|
}
|
|
if (mt === "m.audio") {
|
|
var label = m.voice ? "🎤 Sprachnachricht" : "🎵 " + esc(m.b);
|
|
if (m.m) return '<div class="body" style="font-size:12.5px;color:var(--text2);margin-bottom:2px">' + label + '</div><div class="media-wrap"><audio controls preload="metadata" src="media/' + m.m + '"></audio></div>';
|
|
return '<div class="body">[Audio: ' + esc(m.b) + "]</div>";
|
|
}
|
|
if (mt === "m.file") {
|
|
if (m.m) return '<a class="file-attach" href="media/' + m.m + '" download="' + esc(m.b) + '"><span class="file-icon">📎</span><div class="file-info"><div class="file-name">' + esc(m.b) + '</div><div class="file-size">' + (m.fs || "") + "</div></div></a>";
|
|
return '<div class="body">[Datei: ' + esc(m.b) + "]</div>";
|
|
}
|
|
if (mt === "m.location") {
|
|
if (m.geo) {
|
|
var coords = m.geo.replace("geo:", "").split(";")[0];
|
|
var p = coords.split(",");
|
|
return '<div class="body">📍 <a href="https://www.openstreetmap.org/?mlat=' + p[0] + "&mlon=" + (p[1] || "0") + "#map=16/" + p[0] + "/" + (p[1] || "0") + '" target="_blank" rel="noopener">' + esc(m.b) + "</a></div>";
|
|
}
|
|
return '<div class="body">📍 ' + esc(m.b) + "</div>";
|
|
}
|
|
return '<div class="body">' + esc(m.b || "") + "</div>";
|
|
}
|
|
|
|
function renderMessage(m) {
|
|
if (m.t === "date") {
|
|
lastSender = "";
|
|
return '<div class="date-separator"><span class="date-pill">' + esc(m.text) + "</span></div>";
|
|
}
|
|
if (m.t === "sys") {
|
|
lastSender = "";
|
|
return '<div class="system-event"><span>' + esc(m.text) + "</span></div>";
|
|
}
|
|
if (m.t === "avatar") {
|
|
lastSender = "";
|
|
var inner = "";
|
|
if (m.m) {
|
|
inner += '<img src="media/' + m.m + '" style="width:28px;height:28px;border-radius:50%;vertical-align:middle;margin-right:6px" loading="lazy">';
|
|
}
|
|
inner += esc(m.text);
|
|
return '<div class="system-event"><span>' + inner + "</span></div>";
|
|
}
|
|
if (m.t === "enc") {
|
|
return '<div class="msg-group ' + (m.o ? "own" : "other") + '"><div class="msg"><div class="body encrypted-msg">🔒 Verschlüsselte Nachricht</div><div class="meta"><span class="time">' + m.ts + "</span></div></div></div>";
|
|
}
|
|
if (m.t === "sticker") {
|
|
if (!m.m) return "";
|
|
return '<div class="msg-group ' + (m.o ? "own" : "other") + '"><div class="msg" style="background:transparent;box-shadow:none;padding:4px"><div class="media-wrap"><img src="media/' + m.m + '" alt="' + esc(m.b) + '" style="max-width:160px;max-height:160px" loading="lazy"></div><div class="meta"><span class="time">' + m.ts + "</span></div></div></div>";
|
|
}
|
|
if (m.t === "msg") {
|
|
var cls = m.o ? "own" : "other";
|
|
var sender = "";
|
|
if (!m.o && m.s !== lastSender) {
|
|
sender = '<span class="sender" style="color:' + m.c + '">' + esc(m.n) + "</span>";
|
|
}
|
|
lastSender = m.s;
|
|
var body = m.del ? '<div class="body redacted">Diese Nachricht wurde gelöscht</div>' : renderBody(m);
|
|
return '<div class="msg-group ' + cls + '"><div class="msg">' + sender + body + '<div class="meta"><span class="time">' + m.ts + "</span></div></div></div>";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function renderChunk() {
|
|
var end = Math.min(rendered + CHUNK, msgs.length);
|
|
var html = "";
|
|
for (var i = rendered; i < end; i++) {
|
|
html += renderMessage(msgs[i]);
|
|
}
|
|
var sentinel = document.getElementById("sentinel");
|
|
if (sentinel) sentinel.remove();
|
|
|
|
var frag = document.createElement("div");
|
|
frag.innerHTML = html;
|
|
while (frag.firstChild) chat.appendChild(frag.firstChild);
|
|
|
|
rendered = end;
|
|
|
|
if (rendered < msgs.length) {
|
|
var s = document.createElement("div");
|
|
s.id = "sentinel";
|
|
s.style.height = "1px";
|
|
chat.appendChild(s);
|
|
observer.observe(s);
|
|
}
|
|
}
|
|
|
|
var observer = new IntersectionObserver(function(entries) {
|
|
if (entries[0].isIntersecting) renderChunk();
|
|
}, {rootMargin: "1200px"});
|
|
|
|
renderChunk();
|
|
|
|
function openLightbox(s) {
|
|
var l = document.getElementById("lightbox");
|
|
document.getElementById("lightbox-img").src = s;
|
|
l.classList.add("active");
|
|
}
|
|
function closeLightbox() {
|
|
document.getElementById("lightbox").classList.remove("active");
|
|
}
|
|
document.addEventListener("keydown", function(e) {
|
|
if (e.key === "Escape") closeLightbox();
|
|
});
|
|
|
|
var scrollBtn = document.getElementById("scrollTop");
|
|
window.addEventListener("scroll", function() {
|
|
scrollBtn.classList.toggle("visible", window.scrollY > 800);
|
|
});
|
|
scrollBtn.addEventListener("click", function() {
|
|
window.scrollTo({top: 0, behavior: "smooth"});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|