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;