import { useState, useEffect, useRef } from 'react'; import { api, S } from '../lib.js'; import { UserIcon, TrashIcon } from '../icons.jsx'; import { getOrCreateKeyPair, getPublicKeyJwk, getFingerprint, deriveSharedKey, encryptMsg, decryptMsg, } from '../crypto.js'; export default function Nachrichten({ toast, mobile }) { const [kp, setKp] = useState(null); const [initError, setInitError] = useState(false); const [users, setUsers] = useState([]); const [activeUser, setActiveUser] = useState(null); const [messages, setMessages] = useState([]); const [sharedKey, setSharedKey] = useState(null); // null | CryptoKey | 'error' const [decrypted, setDecrypted] = useState({}); const [partnerFp, setPartnerFp] = useState(''); const [text, setText] = useState(''); const [sending, setSending] = useState(false); const bottomRef = useRef(null); const pollRef = useRef(null); // ── Initialisierung ────────────────────────────────────────────────────────── useEffect(() => { getOrCreateKeyPair() .then(async k => { setKp(k); try { const jwk = await getPublicKeyJwk(k); await api('/tools/nachrichten/keys', { body: { public_key: JSON.stringify(jwk) } }); } catch {} loadUsers(); }) .catch(() => setInitError(true)); }, []); // ── Polling ────────────────────────────────────────────────────────────────── useEffect(() => { clearInterval(pollRef.current); if (!activeUser) return; loadMessages(activeUser.id); pollRef.current = setInterval(() => loadMessages(activeUser.id), 10_000); return () => clearInterval(pollRef.current); }, [activeUser?.id]); // ── Shared Key + Partner-Fingerprint ───────────────────────────────────────── useEffect(() => { setSharedKey(null); setDecrypted({}); setPartnerFp(''); if (!kp || !activeUser?.public_key) return; try { const pubJwk = JSON.parse(activeUser.public_key); deriveSharedKey(kp.privateKey, pubJwk) .then(setSharedKey) .catch(() => setSharedKey('error')); // Fingerprint des Partner-Keys berechnen (aus dem auf dem Server gespeicherten Public Key) crypto.subtle.importKey('jwk', pubJwk, { name:'X25519' }, true, []) .then(k => getFingerprint({ publicKey: k })) .then(setPartnerFp) .catch(() => setPartnerFp('?')); } catch { setSharedKey('error'); } }, [activeUser?.id, kp]); // ── Entschlüsseln ──────────────────────────────────────────────────────────── useEffect(() => { if (sharedKey === null || sharedKey === 'error' || !messages.length) { setDecrypted({}); return; } let cancelled = false; (async () => { const result = {}; for (const m of messages) { if (cancelled) return; try { result[m.id] = await decryptMsg(sharedKey, m.encrypted_content, m.iv); } catch { result[m.id] = null; // null = nicht entschlüsselbar } } if (!cancelled) setDecrypted(result); })(); return () => { cancelled = true; }; }, [messages, sharedKey]); // ── Scroll to bottom ───────────────────────────────────────────────────────── useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages.length]); // ── Handlers ───────────────────────────────────────────────────────────────── const loadUsers = async () => { try { setUsers(await api('/tools/nachrichten/users')); } catch {} }; const loadMessages = async userId => { try { setMessages(await api(`/tools/nachrichten/messages/${userId}`)); await loadUsers(); } catch {} }; const selectUser = u => { setActiveUser(u); setMessages([]); setDecrypted({}); }; const send = async () => { if (!text.trim() || !kp || !activeUser) return; if (!activeUser.public_key) { toast('Empfänger hat noch keinen Schlüssel', 'error'); return; } if (sharedKey === 'error') { toast('Schlüssel inkompatibel – Empfänger muss Schlüssel neu generieren', 'error'); return; } if (!sharedKey) { toast('Schlüssel wird vorbereitet…', 'error'); return; } setSending(true); try { const { encrypted_content, iv } = await encryptMsg(sharedKey, text.trim()); await api(`/tools/nachrichten/messages/${activeUser.id}`, { body: { encrypted_content, iv } }); setText(''); await loadMessages(activeUser.id); } catch(e) { toast(e.message, 'error'); } setSending(false); }; const deleteMsg = async id => { try { await api(`/tools/nachrichten/messages/${id}`, { method:'DELETE' }); setMessages(p => p.filter(m => m.id !== id)); } catch(e) { toast(e.message, 'error'); } }; const deleteConversation = async () => { if (!window.confirm(`Gesamten Verlauf mit ${activeUser.username} löschen?`)) return; try { await api(`/tools/nachrichten/conversation/${activeUser.id}`, { method:'DELETE' }); setMessages([]); await loadUsers(); } catch(e) { toast(e.message, 'error'); } }; // ── Layout ─────────────────────────────────────────────────────────────────── const showList = !mobile || !activeUser; const showChat = !mobile || !!activeUser; const h = mobile ? 'calc(100dvh - 56px - env(safe-area-inset-bottom, 0px))' : '100vh'; if (initError) return (