diff --git a/frontend/src/tools/dateien.jsx b/frontend/src/tools/dateien.jsx
index 0b0d192..ecab45f 100644
--- a/frontend/src/tools/dateien.jsx
+++ b/frontend/src/tools/dateien.jsx
@@ -205,7 +205,7 @@ export default function Dateien({ toast, mobile }) {
{/* ── Tabs + Neu-Ordner (überall gleich) ───────────────────────────── */}
{[
- ['own','Meine Dateien', null],
+ ['own','Dateien', null],
['shared','Geteilt mit mir', sharedCnt||null],
['sharedByMe','Geteilt von mir', byMeCnt||null],
].map(([k,l,cnt])=>(
diff --git a/frontend/src/tools/nachrichten.jsx b/frontend/src/tools/nachrichten.jsx
new file mode 100644
index 0000000..eaaa059
--- /dev/null
+++ b/frontend/src/tools/nachrichten.jsx
@@ -0,0 +1,325 @@
+import { useState, useEffect, useRef, useCallback } from 'react';
+import { api, S } from '../lib.js';
+import { useConfirm } from '../confirm.jsx';
+import { getOrCreateKeyPair, exportPublicKey, encryptMessage, decryptMessage } from '../crypto.js';
+import { SearchIcon, TrashIcon, ChevronIcon } from '../icons.jsx';
+
+const fmtTime = s => new Date(s).toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'});
+const fmtDay = s => {
+ const d = new Date(s), now = new Date();
+ if (d.toDateString()===now.toDateString()) return 'Heute';
+ const y = new Date(now); y.setDate(y.getDate()-1);
+ if (d.toDateString()===y.toDateString()) return 'Gestern';
+ return d.toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit'});
+};
+
+// ── Push-Subscription ─────────────────────────────────────────────────────────
+async function subscribePush(toast) {
+ if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;
+ try {
+ const reg = await navigator.serviceWorker.ready;
+ const data = await api('/nachrichten/vapid-key');
+ const sub = await reg.pushManager.subscribe({
+ userVisibleOnly: true,
+ applicationServerKey: data.publicKey,
+ });
+ await api('/nachrichten/subscribe', { body: sub.toJSON() });
+ } catch(e) { console.warn('Push subscription failed:', e.message); }
+}
+
+// ── Setup: Schlüsselpaar + Server registrieren ────────────────────────────────
+async function ensureSetup(toast) {
+ try {
+ await getOrCreateKeyPair();
+ const pub = await exportPublicKey();
+ await api('/nachrichten/keys', { body: { publicKey: pub } });
+ await subscribePush();
+ return true;
+ } catch(e) { toast('Schlüssel-Setup fehlgeschlagen','error'); return false; }
+}
+
+// ── Conversation View ─────────────────────────────────────────────────────────
+function Conversation({ partner, myUsername, onBack, toast }) {
+ const [messages, setMessages] = useState([]);
+ const [input, setInput] = useState('');
+ const [loading, setLoading] = useState(true);
+ const [sending, setSending] = useState(false);
+ const [theirKey, setTheirKey] = useState(null);
+ const [decrypted, setDecrypted] = useState({});
+ const bottomRef = useRef(null);
+ const { confirm, ConfirmDialog } = useConfirm();
+
+ const loadMessages = useCallback(async () => {
+ try {
+ const [msgs, keyData] = await Promise.all([
+ api(`/nachrichten/with/${partner}`),
+ api(`/nachrichten/keys/${partner}`).catch(()=>null),
+ ]);
+ setMessages(msgs);
+ if (keyData) setTheirKey(keyData.publicKey);
+
+ // Decrypt all messages
+ const dec = {};
+ for (const m of msgs) {
+ try {
+ const partnerKey = m.sender_name === myUsername ? keyData?.publicKey : keyData?.publicKey;
+ // Both sides use the same shared secret derived from the key exchange
+ // But we need to know WHICH key to use for decryption
+ // For messages sent BY me: I encrypted with their public key → decrypt with my private + their public
+ // For messages FROM them: they encrypted with my public key → decrypt with my private + their public
+ // In both cases the shared secret is the same (ECDH is symmetric)
+ if (keyData) {
+ dec[m.id] = await decryptMessage(m.encrypted_content, m.iv, keyData.publicKey);
+ }
+ } catch { dec[m.id] = '🔒 [nicht entschlüsselbar]'; }
+ }
+ setDecrypted(dec);
+ } catch(e) { toast(e.message,'error'); } finally { setLoading(false); }
+ }, [partner, myUsername]);
+
+ useEffect(() => { loadMessages(); }, [loadMessages]);
+ useEffect(() => { bottomRef.current?.scrollIntoView({behavior:'smooth'}); }, [messages]);
+
+ const send = async () => {
+ if (!input.trim() || !theirKey) return;
+ setSending(true);
+ try {
+ const { encrypted_content, iv } = await encryptMessage(input.trim(), theirKey);
+ await api('/nachrichten/send', { body: { recipient_username: partner, encrypted_content, iv } });
+ setInput('');
+ await loadMessages();
+ } catch(e) { toast(e.message,'error'); } finally { setSending(false); }
+ };
+
+ const delMsg = async id => {
+ if (!await confirm('Nachricht für dich löschen?')) return;
+ try { await api(`/nachrichten/${id}`,{method:'DELETE'}); setMessages(p=>p.filter(m=>m.id!==id)); }
+ catch(e) { toast(e.message,'error'); }
+ };
+
+ const delAll = async () => {
+ if (!await confirm(`Gesamtes Gespräch mit ${partner} löschen? Es verschwindet nur bei dir.`)) return;
+ try { await api(`/nachrichten/conversation/${partner}`,{method:'DELETE'}); onBack(); }
+ catch(e) { toast(e.message,'error'); }
+ };
+
+ let lastDay = null;
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
{partner}
+ {!theirKey &&
⚠ Kein Schlüssel – kann noch nicht senden
}
+ {theirKey &&
🔒 Ende-zu-Ende verschlüsselt
}
+
+
+
+
+ {/* Messages */}
+
+ {loading &&
Lädt…
}
+ {!loading && messages.length===0 && (
+
+
🔒
+
Noch keine Nachrichten.
Alle Nachrichten sind Ende-zu-Ende verschlüsselt.
+
+ )}
+ {messages.map(msg => {
+ const isMe = msg.sender_name === myUsername;
+ const day = fmtDay(msg.created_at);
+ const showDay = day !== lastDay; lastDay = day;
+ return (
+
+ {showDay && (
+
+ {day}
+
+ )}
+
+ {!isMe &&
}
+
+
+
+ {decrypted[msg.id] || '🔒 …'}
+
+
+
+ {fmtTime(msg.created_at)}
+
+
+
+
+
+ );
+ })}
+
+
+
+ {/* Input */}
+
+
+
+ );
+}
+
+// ── Hauptansicht: Gesprächsliste ──────────────────────────────────────────────
+export default function Nachrichten({ toast, mobile, user }) {
+ const [convs, setConvs] = useState([]);
+ const [users, setUsers] = useState([]);
+ const [openPartner, setOpen] = useState(null);
+ const [search, setSearch] = useState('');
+ const [newChat, setNewChat] = useState(false);
+ const [ready, setReady] = useState(false);
+ const { confirm, ConfirmDialog } = useConfirm();
+
+ useEffect(() => {
+ ensureSetup(toast).then(ok => {
+ setReady(ok);
+ if (ok) {
+ api('/nachrichten/conversations').then(setConvs).catch(()=>{});
+ api('/nachrichten/users').then(setUsers).catch(()=>{});
+ }
+ });
+ // Refresh every 10s
+ const t = setInterval(() => {
+ api('/nachrichten/conversations').then(setConvs).catch(()=>{});
+ }, 10000);
+ return () => clearInterval(t);
+ }, []);
+
+ const startChat = username => {
+ setNewChat(false); setSearch(''); setOpen(username);
+ };
+
+ if (openPartner) return (
+
{setOpen(null);api('/nachrichten/conversations').then(setConvs);}} toast={toast}/>
+ );
+
+ const filtered = convs.filter(c => c.partner_name.toLowerCase().includes(search.toLowerCase()));
+ const newFiltered = users.filter(u => u.username.toLowerCase().includes(search.toLowerCase()) && !convs.find(c=>c.partner_name===u.username));
+
+ return (
+
+
+
+
+
Nachrichten
+
🔒 Ende-zu-Ende verschlüsselt
+
+
+
+
+ {!ready && (
+
+ Initialisiere Verschlüsselung…
+
+ )}
+
+ {/* Suche */}
+
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Person suchen…"
+ style={{...S.inp,paddingLeft:34,fontSize:15}}/>
+
+
+ {/* Neues Gespräch – User-Picker */}
+ {newChat && (
+
+
NEUES GESPRÄCH
+ {users.filter(u=>u.username.toLowerCase().includes(search.toLowerCase())).map(u=>(
+
+ ))}
+
+ )}
+
+ {/* Gesprächsliste */}
+ {filtered.length===0&&!newChat&&(
+
+
💬
+
+ Noch keine Nachrichten.
Klicke auf "+ Neu" um ein Gespräch zu starten.
+
+
+ )}
+ {filtered.map(conv => (
+
startChat(conv.partner_name)}
+ style={{...S.card,marginBottom:6,padding:'12px 14px',cursor:'pointer',
+ display:'flex',alignItems:'center',gap:12,
+ border:conv.unread>0?'1px solid rgba(78,205,196,0.2)':'1px solid rgba(255,255,255,0.07)'}}>
+
+ {conv.partner_name[0].toUpperCase()}
+
+
+
+ {conv.partner_name}
+
+ {fmtDay(conv.last_at)}
+
+
+
+ 🔒 Verschlüsselt
+ {conv.unread>0&&(
+
+ {conv.unread}
+
+ )}
+
+
+
+
+ ))}
+
+ );
+}