diff --git a/src/lib/components/EncryptionWarning.jsx b/src/lib/components/EncryptionWarning.jsx index b8bd86e..1427454 100644 --- a/src/lib/components/EncryptionWarning.jsx +++ b/src/lib/components/EncryptionWarning.jsx @@ -1,17 +1,21 @@ 'use client'; import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; import { keyManager } from '@/lib/crypto/key-manager.js'; export default function EncryptionWarning() { + const router = useRouter(); const [hasKeys, setHasKeys] = useState(true); const [checked, setChecked] = useState(false); + const [generating, setGenerating] = useState(false); + const [error, setError] = useState(''); useEffect(() => { async function check() { try { - const has = await keyManager.hasUserKeys(); - setHasKeys(has); + await keyManager.initialize(); + setHasKeys(await keyManager.hasUserKeys()); } catch { setHasKeys(false); } @@ -20,20 +24,72 @@ export default function EncryptionWarning() { check(); }, []); + async function handleGenerate() { + // New keys give you a fresh identity but make every message that was + // encrypted to your previous key permanently unreadable. Restoring/uploading + // existing keys is almost always what the user actually wants. + const ok = window.confirm( + 'Generate brand-new keys?\n\nYou will NOT be able to read any past messages or files that were encrypted to your previous keys. If you have a backup or an exported key file, use "Upload existing keys" instead.' + ); + if (!ok) return; + setGenerating(true); + setError(''); + try { + await keyManager.generateUserKeys(); + setHasKeys(true); + } catch (err) { + setError(err?.message || 'Failed to generate keys'); + } finally { + setGenerating(false); + } + } + if (!checked || hasKeys) return null; return (
⚠️
- Encryption keys not found -

Your messages cannot be encrypted. Please go to Settings to generate or restore your encryption keys.

+ No encryption keys on this device +

+ This browser doesn't have your encryption keys, so messages can't be decrypted or sent + securely. Upload your existing keys to keep your history, or generate new ones to start fresh. +

+ {error &&

{error}

} +
+ + +
+

+ Switched browsers or devices? Choose Upload existing keys — generating new keys + loses access to all past messages. +

); diff --git a/src/lib/components/chat/MessageAttachments.jsx b/src/lib/components/chat/MessageAttachments.jsx index a39d8df..cb2b054 100644 --- a/src/lib/components/chat/MessageAttachments.jsx +++ b/src/lib/components/chat/MessageAttachments.jsx @@ -35,6 +35,19 @@ export default function MessageAttachments({ messageId }) { const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); + // Bumped to force a re-fetch when an upload for this message completes after + // the component already mounted (the carrier 'file' message renders — and + // fetches — before its attachments finish uploading). + const [reloadKey, setReloadKey] = useState(0); + + useEffect(() => { + if (!messageId) return undefined; + const onUpdated = (e) => { + if (e.detail?.messageId === messageId) setReloadKey((k) => k + 1); + }; + window.addEventListener('attachments:updated', onUpdated); + return () => window.removeEventListener('attachments:updated', onUpdated); + }, [messageId]); useEffect(() => { if (!messageId) return undefined; @@ -71,7 +84,7 @@ export default function MessageAttachments({ messageId }) { cancelled = true; createdUrls.forEach((u) => URL.revokeObjectURL(u)); }; - }, [messageId]); + }, [messageId, reloadKey]); if (loading) { return
Decrypting attachment…
; diff --git a/src/lib/components/chat/MessageInput.jsx b/src/lib/components/chat/MessageInput.jsx index b06231e..cf582c3 100644 --- a/src/lib/components/chat/MessageInput.jsx +++ b/src/lib/components/chat/MessageInput.jsx @@ -203,6 +203,13 @@ export default function MessageInput({ conversationId, disabled = false }) { } catch { // non-fatal: realtime will reconcile } + + // The carrier 'file' message mounted (and fetched its attachments) + // before these uploads finished, so tell it to re-fetch now that the + // files exist — otherwise it stays empty until a full page refresh. + if (typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent('attachments:updated', { detail: { messageId } })); + } trackMessageSent({ conversationId, type: 'file', hasAttachments: true }); setSelectedFiles([]); setMessageText(''); diff --git a/src/lib/crypto/key-sync-service.js b/src/lib/crypto/key-sync-service.js index 313a7ab..a9ef115 100644 --- a/src/lib/crypto/key-sync-service.js +++ b/src/lib/crypto/key-sync-service.js @@ -148,14 +148,22 @@ export class KeySyncService { return false; } - // Check if our key exists in the database - const hasKeyInDb = data.public_keys && data.public_keys[currentUserId]; - - if (!hasKeyInDb) { + // Check if our key exists in the database AND matches the local key. + // Presence alone isn't enough: after switching browsers / rotating keys + // the DB still holds the OLD public key, so a presence-only check would + // skip the sync and leave everyone encrypting to a dead key. + const dbKey = data.public_keys && data.public_keys[currentUserId]; + + if (!dbKey) { console.log('🔑 Public key not found in database, sync needed'); return true; } + if (dbKey !== publicKey) { + console.log('🔑 Database public key differs from local key (rotated/new device), sync needed'); + return true; + } + console.log('🔑 Public key already synced to database'); return false;