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 */} +
+