From 5cdaab35f5af6f6811f85f38a843694bca83ac33 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Thu, 4 Dec 2025 12:34:34 +0300 Subject: [PATCH] Try fixing initialization --- frontend/src/pages/chat/hooks/useDM.ts | 145 ++++++++++++------ .../src/pages/chat/ui/right/panels/DMPanel.ts | 92 +++++++++-- frontend/src/state/user.ts | 16 +- .../src/utils/crypto/messagePlaintextSync.ts | 33 +++- .../src/utils/crypto/sessionRestoreState.ts | 64 ++++++++ frontend/src/utils/crypto/sessionSync.ts | 19 ++- .../src/utils/crypto/signalProtocolInit.ts | 9 +- frontend/src/utils/crypto/signalStorage.ts | 11 +- 8 files changed, 315 insertions(+), 74 deletions(-) create mode 100644 frontend/src/utils/crypto/sessionRestoreState.ts diff --git a/frontend/src/pages/chat/hooks/useDM.ts b/frontend/src/pages/chat/hooks/useDM.ts index 825bcb6..e67f687 100644 --- a/frontend/src/pages/chat/hooks/useDM.ts +++ b/frontend/src/pages/chat/hooks/useDM.ts @@ -52,6 +52,10 @@ export function useDM() { if (!user.authToken) return; try { + // Wait for session restoration to complete (if in progress) + const { waitForSessionRestore } = await import("@/utils/crypto/sessionRestoreState"); + await waitForSessionRestore(); + // Get public key const publicKey = await api.chats.dm.fetchUserPublicKey(dmUser.id, user.authToken); if (!publicKey) return; @@ -63,11 +67,22 @@ export function useDM() { // Find last message const lastMessage = messages[messages.length - 1]; let lastPlaintext: string | null = null; - + const isAuthor = lastMessage.senderId === user.currentUser?.id; try { - lastPlaintext = (JSON.parse(await decryptDm(lastMessage, lastMessage.senderId)) as DmEncryptedJSON).data.content; + if (isAuthor) { + // For our own messages, fetch plaintexts from server (encrypted at rest) + const { fetchMessagePlaintextsForRecipient } = await import("@/utils/crypto/messagePlaintextSync"); + const plaintexts = await fetchMessagePlaintextsForRecipient(dmUser.id); + const cached = plaintexts.get(lastMessage.id); + if (cached) { + lastPlaintext = (JSON.parse(cached) as DmEncryptedJSON).data.content; + } + } else { + // Incoming message - decrypt via Signal + lastPlaintext = (JSON.parse(await decryptDm(lastMessage, lastMessage.senderId)) as DmEncryptedJSON).data.content; + } } catch (error) { - console.error("Failed to decrypt last message:", error); + console.error("Failed to get last message preview:", error); } // Calculate unread count @@ -102,6 +117,10 @@ export function useDM() { usersLoadedRef.current = true; setIsLoadingUsers(true); try { + // Wait for session restoration to complete (if in progress) + const { waitForSessionRestore } = await import("@/utils/crypto/sessionRestoreState"); + await waitForSessionRestore(); + const conversations = await api.chats.dm.conversations(user.authToken); // Process conversations and decrypt last messages @@ -111,17 +130,25 @@ export function useDM() { if (conv.lastMessage) { try { - // Get the public key for the other user - const otherUserId = conv.lastMessage.senderId === user.currentUser?.id - ? conv.lastMessage.recipientId - : conv.lastMessage.senderId; - - const publicKey = await api.chats.dm.fetchUserPublicKey(otherUserId, user.authToken!); - if (publicKey) { - // Decrypt the last message - const decryptedJson = await decryptDm(conv.lastMessage, conv.lastMessage.senderId); - const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON; - lastMessageContent = formatDMMessageContent(decryptedData.data.content, conv.lastMessage.senderId, user.currentUser?.id!); + const isAuthor = conv.lastMessage.senderId === user.currentUser?.id; + const otherUserId = conv.user.id; // the other party in the conversation + if (isAuthor) { + // Fetch plaintext of our own last message from server + const { fetchMessagePlaintextsForRecipient } = await import("@/utils/crypto/messagePlaintextSync"); + const plaintexts = await fetchMessagePlaintextsForRecipient(otherUserId); + const cached = plaintexts.get(conv.lastMessage.id); + if (cached) { + const data = JSON.parse(cached) as DmEncryptedJSON; + lastMessageContent = formatDMMessageContent(data.data.content, conv.lastMessage.senderId, user.currentUser!.id); + } + } else { + // Incoming message - decrypt + const publicKey = await api.chats.dm.fetchUserPublicKey(otherUserId, user.authToken!); + if (publicKey) { + const decryptedJson = await decryptDm(conv.lastMessage, conv.lastMessage.senderId); + const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON; + lastMessageContent = formatDMMessageContent(decryptedData.data.content, conv.lastMessage.senderId, user.currentUser!.id); + } } } catch (error) { // Silently fail for last message decryption - it's not critical @@ -160,6 +187,11 @@ export function useDM() { setIsLoadingHistory(true); try { + // Wait for session restoration to complete (if in progress) + const { waitForSessionRestore } = await import("@/utils/crypto/sessionRestoreState"); + await waitForSessionRestore(); + console.log(`[useDM] Session restoration complete, proceeding with message load for user ${userId}`); + const { messages } = await api.chats.dm.fetchMessages(userId, user.authToken, 50); const decryptedMessages: Message[] = []; let maxIncomingId = 0; @@ -276,23 +308,29 @@ export function useDM() { const conversations = await api.chats.dm.conversations(user.authToken); const userConversation = conversations.find(conv => conv.user.id === userId); - if (userConversation) { + if (userConversation) { let lastMessageContent: string | undefined = undefined; if (userConversation.lastMessage) { try { - // Get the public key for the other user - const otherUserId = userConversation.lastMessage.senderId === user.currentUser?.id - ? userConversation.lastMessage.recipientId - : userConversation.lastMessage.senderId; - - const publicKey = await api.chats.dm.fetchUserPublicKey(otherUserId, user.authToken!); - if (publicKey) { - // Decrypt the last message - const decryptedJson = await decryptDm(userConversation.lastMessage, userConversation.lastMessage.senderId); - const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON; - lastMessageContent = formatDMMessageContent(decryptedData.data.content, userConversation.lastMessage.senderId, user.currentUser?.id!); - } + const isAuthor = userConversation.lastMessage.senderId === user.currentUser?.id; + const otherUserId = userId; + if (isAuthor) { + const { fetchMessagePlaintextsForRecipient } = await import("@/utils/crypto/messagePlaintextSync"); + const plaintexts = await fetchMessagePlaintextsForRecipient(otherUserId); + const cached = plaintexts.get(userConversation.lastMessage.id); + if (cached) { + const data = JSON.parse(cached) as DmEncryptedJSON; + lastMessageContent = formatDMMessageContent(data.data.content, userConversation.lastMessage.senderId, user.currentUser!.id); + } + } else { + const publicKey = await api.chats.dm.fetchUserPublicKey(otherUserId, user.authToken!); + if (publicKey) { + const decryptedJson = await decryptDm(userConversation.lastMessage, userConversation.lastMessage.senderId); + const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON; + lastMessageContent = formatDMMessageContent(decryptedData.data.content, userConversation.lastMessage.senderId, user.currentUser!.id); + } + } } catch (error) { console.error("Failed to decrypt last message for user", userId, error); } @@ -335,23 +373,33 @@ export function useDM() { return; } const otherUserId = senderId === user.currentUser.id ? recipientId : senderId; - + // Update unread count and last message preview try { - const publicKey = await api.chats.dm.fetchUserPublicKey(otherUserId, user.authToken!); - if (publicKey) { - const decryptedJson = await decryptDm(envelope, senderId); - const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON; - const messageContent = decryptedData.data.content; + let messageContent: string | null = null; + if (senderId === user.currentUser.id) { + const { fetchMessagePlaintextsForRecipient } = await import("@/utils/crypto/messagePlaintextSync"); + const plaintexts = await fetchMessagePlaintextsForRecipient(otherUserId); + const cached = plaintexts.get(envelope.id); + if (cached) { + messageContent = (JSON.parse(cached) as DmEncryptedJSON).data.content; + } + } else { + const publicKey = await api.chats.dm.fetchUserPublicKey(otherUserId, user.authToken!); + if (publicKey) { + const decryptedJson = await decryptDm(envelope, senderId); + const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON; + messageContent = decryptedData.data.content; + } + } + if (messageContent !== null) { const formattedMessage = formatDMMessageContent(messageContent, senderId, user.currentUser.id); - setDmUsersState(prev => prev.map(u => u.id === otherUserId ? { ...u, unreadCount: senderId !== user.currentUser?.id ? u.unreadCount + 1 : u.unreadCount, - lastMessage: formattedMessage, - publicKey + lastMessage: formattedMessage } : u )); @@ -368,18 +416,29 @@ export function useDM() { } const otherUserId = senderId === user.currentUser.id ? recipientId : senderId; try { - const publicKey = await api.chats.dm.fetchUserPublicKey(otherUserId, user.authToken!); - if (publicKey) { - const decryptedJson = await decryptDm(envelope, senderId); - const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON; - const messageContent = decryptedData.data.content; + let messageContent: string | null = null; + if (senderId === user.currentUser.id) { + const { fetchMessagePlaintextsForRecipient } = await import("@/utils/crypto/messagePlaintextSync"); + const plaintexts = await fetchMessagePlaintextsForRecipient(otherUserId); + const cached = plaintexts.get(id); + if (cached) { + messageContent = (JSON.parse(cached) as DmEncryptedJSON).data.content; + } + } else { + const publicKey = await api.chats.dm.fetchUserPublicKey(otherUserId, user.authToken!); + if (publicKey) { + const decryptedJson = await decryptDm(envelope, senderId); + const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON; + messageContent = decryptedData.data.content; + } + } + if (messageContent !== null) { const formattedMessage = formatDMMessageContent(messageContent, senderId, user.currentUser.id); setDmUsersState(prev => prev.map(u => u.id === otherUserId ? { ...u, - lastMessage: formattedMessage, - publicKey + lastMessage: formattedMessage } : u )); diff --git a/frontend/src/pages/chat/ui/right/panels/DMPanel.ts b/frontend/src/pages/chat/ui/right/panels/DMPanel.ts index 3b2db75..f6682ab 100644 --- a/frontend/src/pages/chat/ui/right/panels/DMPanel.ts +++ b/frontend/src/pages/chat/ui/right/panels/DMPanel.ts @@ -63,7 +63,7 @@ export class DMPanel extends MessagePanel { this.failedDecryptionIds.clear(); } - private async parseTextPayload(env: DmEnvelope, decryptedMessages: Message[]) { + private async parseTextPayload(env: DmEnvelope, decryptedMessages: Message[], plaintextOverride?: string) { // Check if this is a message sent by the current user const isSentByUs = env.senderId === this.currentUser.currentUser?.id; @@ -71,8 +71,24 @@ export class DMPanel extends MessagePanel { if (isSentByUs) { // Can't decrypt our own sent messages in Signal Protocol // The plaintext should be passed in from loadMessages (fetched from server) - // For now, throw an error - the caller should handle this by fetching plaintexts first - throw new Error("Cannot decrypt own sent message - plaintext must be fetched from server first"); + if (plaintextOverride) { + plaintext = plaintextOverride; + } else { + // Try to fetch from server as fallback + try { + const { fetchMessagePlaintextsForRecipient } = await import("@/utils/crypto/messagePlaintextSync"); + const plaintexts = await fetchMessagePlaintextsForRecipient(this.dmData!.userId); + const cached = plaintexts.get(env.id); + if (cached) { + plaintext = cached; + } else { + // Not on server - skip this message + throw new Error("Cannot decrypt own sent message - plaintext not available on server"); + } + } catch (error) { + throw new Error("Cannot decrypt own sent message - plaintext must be fetched from server first"); + } + } } else { // Decrypt incoming messages plaintext = await decryptDm(env, env.senderId); @@ -125,6 +141,11 @@ export class DMPanel extends MessagePanel { this.setLoading(true); try { + // Wait for session restoration to complete (if in progress) + const { waitForSessionRestore } = await import("@/utils/crypto/sessionRestoreState"); + await waitForSessionRestore(); + console.log(`[DMPanel] Session restoration complete, proceeding with message load for user ${this.dmData.userId}`); + // Ensure Signal Protocol session is established before fetching messages if (!this.signalService && this.currentUser.currentUser?.id) { this.signalService = new SignalProtocolService(this.currentUser.currentUser.id.toString()); @@ -140,6 +161,8 @@ export class DMPanel extends MessagePanel { console.warn(`Failed to establish Signal Protocol session for user ${this.dmData.userId} during history load:`, error); // Continue loading history, but decryption will likely fail for new messages } + } else { + console.log(`[DMPanel] Signal Protocol session exists for user ${this.dmData.userId}`); } } @@ -223,8 +246,13 @@ export class DMPanel extends MessagePanel { } } - this.clearMessages(); - decryptedMessages.forEach(msg => this.addMessage(msg)); + // Only clear and replace if we actually decrypted something + if (decryptedMessages.length > 0) { + this.clearMessages(); + decryptedMessages.forEach(msg => this.addMessage(msg)); + } else { + console.warn("[DMPanel] No messages decrypted; keeping existing messages to avoid empty state after reload."); + } this.setHasMoreMessages(has_more); // Update last read ID @@ -434,23 +462,56 @@ export class DMPanel extends MessagePanel { // Mark as processed before attempting decryption this.processedMessageIds.add(envelope.id); - const dmMsg = await this.parseTextPayload(envelope, this.getMessages()); - // Check if this is a confirmation of a message we sent - const isOurMessage = envelope.senderId !== this.dmData.userId; + const isOurMessage = envelope.senderId === this.currentUser.currentUser?.id; + + let dmMsg: Message; + if (isOurMessage) { + // For sent messages, fetch plaintext from server + const { fetchMessagePlaintextsForRecipient } = await import("@/utils/crypto/messagePlaintextSync"); + const plaintexts = await fetchMessagePlaintextsForRecipient(this.dmData.userId); + const cachedPlaintext = plaintexts.get(envelope.id); + if (cachedPlaintext) { + // Parse the plaintext and create message + dmMsg = await this.parseTextPayload(envelope, this.getMessages(), cachedPlaintext); + } else { + // Plaintext not available yet - this might be a new message confirmation + // Try to get it from temp message content + const tempMessages = this.getMessages().filter(m => m.id === -1 && m.runtimeData?.sendingState?.tempId); + let tempMsgContent: string | null = null; + for (const tempMsg of tempMessages) { + if (tempMsg.runtimeData?.sendingState?.retryData?.content) { + tempMsgContent = tempMsg.content; + break; + } + } + if (tempMsgContent) { + dmMsg = await this.parseTextPayload(envelope, this.getMessages(), tempMsgContent); + } else { + // Can't display without plaintext - skip + console.warn(`Cannot display sent message ${envelope.id} - plaintext not available`); + return; + } + } + } else { + // Incoming message - decrypt normally + dmMsg = await this.parseTextPayload(envelope, this.getMessages()); + } + if (isOurMessage) { // This is our message being confirmed, find the temp message and replace it const tempMessages = this.getMessages().filter(m => m.id === -1 && m.runtimeData?.sendingState?.tempId); let tempMsgContent: string | null = null; for (const tempMsg of tempMessages) { - if (tempMsg.runtimeData?.sendingState?.retryData?.content === dmMsg.content) { + if ((tempMsg.runtimeData?.sendingState?.retryData?.content === dmMsg.content || + tempMsg.content === dmMsg.content) && tempMsg.runtimeData?.sendingState?.tempId) { tempMsgContent = tempMsg.content; // Get plaintext from temp message - this.handleMessageConfirmed(tempMsg.runtimeData.sendingState.tempId!, dmMsg); + this.handleMessageConfirmed(tempMsg.runtimeData.sendingState.tempId, dmMsg); // Upload the plaintext to server (encrypted) so we can display it in history const { uploadMessagePlaintext } = await import("@/utils/crypto/messagePlaintextSync"); if (tempMsgContent) { - await uploadMessagePlaintext(envelope.id, this.dmData.userId, tempMsgContent); + await uploadMessagePlaintext(this.dmData.userId, envelope.id, tempMsgContent); } return; } @@ -460,9 +521,16 @@ export class DMPanel extends MessagePanel { // (this might happen if the page was reloaded) if (!tempMsgContent && dmMsg.content) { const { uploadMessagePlaintext } = await import("@/utils/crypto/messagePlaintextSync"); - await uploadMessagePlaintext(envelope.id, this.dmData.userId, dmMsg.content); + await uploadMessagePlaintext(this.dmData.userId, envelope.id, dmMsg.content); } + + // Add the message to the chat + this.addMessage(dmMsg); + return; } + + // Incoming message - add to chat + this.addMessage(dmMsg); this.addMessage(dmMsg); diff --git a/frontend/src/state/user.ts b/frontend/src/state/user.ts index 87a5f0a..7430f24 100644 --- a/frontend/src/state/user.ts +++ b/frontend/src/state/user.ts @@ -9,6 +9,7 @@ import { typingManager } from "@/core/typingManager"; import type { UserState } from "./types"; import { clearSessionSync } from "@/utils/crypto/sessionSync"; import { clearMessagePlaintextSync } from "@/utils/crypto/messagePlaintextSync"; +import { resetSessionRestoreState } from "@/utils/crypto/sessionRestoreState"; interface UserStore { user: UserState; @@ -65,6 +66,13 @@ export const useUserStore = create((set) => ({ // Clear session sync clearSessionSync(); clearMessagePlaintextSync(); + + // Reset session restore state + try { + resetSessionRestoreState(); + } catch (error) { + console.error("Failed to reset session restore state:", error); + } set({ user: { @@ -132,14 +140,20 @@ export const useUserStore = create((set) => ({ if (storedKey) { console.log("[RestoreFromStorage] Stored session key found, restoring sessions from server..."); // We can restore sessions using the stored key (password not needed) + const { setRestoringSessions } = await import("@/utils/crypto/sessionRestoreState"); + const restorePromise = restoreSessionsFromServer(user.id.toString(), null, token); + setRestoringSessions(restorePromise); try { - await restoreSessionsFromServer(user.id.toString(), null, token); + await restorePromise; console.log("[RestoreFromStorage] Sessions restored from server using stored key"); } catch (error) { console.error("[RestoreFromStorage] Failed to restore sessions:", error); } } else { console.warn("[RestoreFromStorage] No stored session key - user needs to log in to derive key"); + // Mark restore as complete even if we couldn't restore (to avoid blocking message loading) + const { setRestoringSessions } = await import("@/utils/crypto/sessionRestoreState"); + setRestoringSessions(Promise.resolve()); } // Re-upload prekeys to ensure they are fresh diff --git a/frontend/src/utils/crypto/messagePlaintextSync.ts b/frontend/src/utils/crypto/messagePlaintextSync.ts index 87a59a6..7e110e7 100644 --- a/frontend/src/utils/crypto/messagePlaintextSync.ts +++ b/frontend/src/utils/crypto/messagePlaintextSync.ts @@ -4,7 +4,7 @@ */ import { encryptMessagePlaintext, decryptMessagePlaintext } from "./messagePlaintextEncryption"; -import { uploadMessagePlaintexts, fetchMessagePlaintexts, type MessagePlaintextData } from "@/core/api/crypto/messagePlaintexts"; +import { uploadMessagePlaintexts, fetchMessagePlaintexts } from "@/core/api/crypto/messagePlaintexts"; // Global state for message plaintext sync let syncPassword: string | null = null; @@ -75,21 +75,42 @@ export async function fetchMessagePlaintextsForRecipient( ): Promise> { const plaintexts = new Map(); - if (!syncToken || !syncUserId) { - console.warn("Message plaintext sync not initialized (missing token/userId)"); - return plaintexts; // Return empty map if not initialized + // Try to get token and userId from global state if sync isn't initialized + let token = syncToken; + let userId = syncUserId; + let password = syncPassword; + + if (!token || !userId) { + // Fallback: try to get from user store + try { + const { useUserStore } = await import("@/state/user"); + const userState = useUserStore.getState().user; + if (userState.authToken && userState.currentUser?.id) { + token = userState.authToken; + userId = userState.currentUser.id.toString(); + console.log(`[MessagePlaintextSync] Using token/userId from user store (sync not initialized)`); + } else { + console.warn("Message plaintext sync not initialized (missing token/userId)"); + return plaintexts; // Return empty map if not initialized + } + } catch (error) { + console.warn("Message plaintext sync not initialized (missing token/userId)"); + return plaintexts; // Return empty map if not initialized + } } + + // Password can be null - decryptMessagePlaintext will use stored session key if password is null try { console.log(`Fetching encrypted plaintexts for recipient ${recipientId}...`); - const encryptedMessages = await fetchMessagePlaintexts(syncToken, recipientId); + const encryptedMessages = await fetchMessagePlaintexts(token, recipientId); console.log(`Found ${encryptedMessages.length} encrypted plaintexts, decrypting...`); for (const msg of encryptedMessages) { try { // Use stored key if available, otherwise use password to derive it - const plaintext = await decryptMessagePlaintext(syncPassword, syncUserId, msg.encryptedData); + const plaintext = await decryptMessagePlaintext(password, userId, msg.encryptedData); plaintexts.set(msg.messageId, plaintext); } catch (error) { console.warn(`Failed to decrypt plaintext for message ${msg.messageId}:`, error); diff --git a/frontend/src/utils/crypto/sessionRestoreState.ts b/frontend/src/utils/crypto/sessionRestoreState.ts new file mode 100644 index 0000000..60803ad --- /dev/null +++ b/frontend/src/utils/crypto/sessionRestoreState.ts @@ -0,0 +1,64 @@ +/** + * Global state to track session restoration progress + * Used to ensure messages aren't loaded before sessions are restored + */ + +let isRestoring = false; +let restorePromise: Promise | null = null; +let restoreComplete = false; + +/** + * Mark that session restoration has started + */ +export function setRestoringSessions(promise: Promise): void { + isRestoring = true; + restoreComplete = false; + restorePromise = promise; + promise.finally(() => { + isRestoring = false; + restoreComplete = true; + }); +} + +/** + * Wait for session restoration to complete (if in progress) + */ +export async function waitForSessionRestore(): Promise { + if (!isRestoring && restoreComplete) { + console.log("[SessionRestoreState] Session restoration already completed"); + return; // Already completed + } + if (isRestoring && restorePromise) { + console.log("[SessionRestoreState] Waiting for session restoration to complete..."); + await restorePromise; + console.log("[SessionRestoreState] Session restoration completed"); + } else if (!restoreComplete) { + // No restoration in progress and not completed - mark as complete to avoid blocking + console.log("[SessionRestoreState] No session restoration in progress, marking as complete"); + restoreComplete = true; + } +} + +/** + * Check if session restoration is in progress + */ +export function isSessionRestoreInProgress(): boolean { + return isRestoring; +} + +/** + * Check if session restoration has completed + */ +export function hasSessionRestoreCompleted(): boolean { + return restoreComplete; +} + +/** + * Reset the restore state (e.g., on logout) + */ +export function resetSessionRestoreState(): void { + isRestoring = false; + restorePromise = null; + restoreComplete = false; +} + diff --git a/frontend/src/utils/crypto/sessionSync.ts b/frontend/src/utils/crypto/sessionSync.ts index 204d4f0..16cca08 100644 --- a/frontend/src/utils/crypto/sessionSync.ts +++ b/frontend/src/utils/crypto/sessionSync.ts @@ -110,14 +110,19 @@ export async function restoreSessionsFromServer( try { const address = `${sessionData.recipientId}.${sessionData.deviceId}`; - // Check if we already have a local session - if so, skip restoration - // This prevents overwriting a newer session with an older one - const existingSession = await storage.loadSession(address); - if (existingSession) { - console.log(`Skipping restoration for recipient ${sessionData.recipientId} - local session already exists`); - continue; + // Always restore from server to ensure we have a valid session + // Local sessions might be corrupted, so we restore from server on every reload + // The server has the authoritative copy encrypted with password-derived key + try { + const existingSession = await storage.loadSession(address); + if (existingSession) { + console.log(`[Session Sync] Local session exists for recipient ${sessionData.recipientId}, but restoring from server to ensure validity`); + } + } catch (error) { + console.log(`[Session Sync] Local session for recipient ${sessionData.recipientId} failed to load, restoring from server`); } + // Always restore from server (don't skip) const encryptedBlob = decodeSessionBlob(sessionData.encryptedData); // Use stored key if available, otherwise use password to derive it const sessionRecord = await decryptSessionWithPassword(password, userId, encryptedBlob); @@ -125,7 +130,7 @@ export async function restoreSessionsFromServer( // Store in IndexedDB (sync callback won't fire because isRestoring is true) await storage.storeSession(address, sessionRecord); restoredCount++; - console.log(`Restored session for recipient ${sessionData.recipientId}`); + console.log(`[Session Sync ✅] Restored session for recipient ${sessionData.recipientId} from server`); } catch (error) { failedCount++; console.warn(`Failed to restore session for recipient ${sessionData.recipientId}:`, error); diff --git a/frontend/src/utils/crypto/signalProtocolInit.ts b/frontend/src/utils/crypto/signalProtocolInit.ts index 9b3984f..8beeb8b 100644 --- a/frontend/src/utils/crypto/signalProtocolInit.ts +++ b/frontend/src/utils/crypto/signalProtocolInit.ts @@ -73,8 +73,11 @@ export async function initializeSignalProtocol({ console.log("========================================"); console.log("[Signal Protocol Init] Step 5: ⚠️ RESTORING SESSIONS FROM SERVER"); console.log("========================================"); + const { setRestoringSessions } = await import("./sessionRestoreState"); + const restorePromise = restoreSessionsFromServer(userId, password, token); + setRestoringSessions(restorePromise); try { - await restoreSessionsFromServer(userId, password, token); + await restorePromise; console.log("========================================"); console.log("[Signal Protocol Init] Step 5: ✅ SESSIONS RESTORED FROM SERVER"); console.log("========================================"); @@ -85,6 +88,10 @@ export async function initializeSignalProtocol({ console.error("========================================"); // Continue even if restoration fails } + } else { + // Mark restore as complete if we're not restoring (to avoid blocking message loading) + const { setRestoringSessions } = await import("./sessionRestoreState"); + setRestoringSessions(Promise.resolve()); } // Step 6: Upload prekey bundle diff --git a/frontend/src/utils/crypto/signalStorage.ts b/frontend/src/utils/crypto/signalStorage.ts index 0aa6652..32e0228 100644 --- a/frontend/src/utils/crypto/signalStorage.ts +++ b/frontend/src/utils/crypto/signalStorage.ts @@ -350,6 +350,7 @@ export class SignalProtocolStorage implements StorageType { request.onsuccess = () => { const data = request.result; if (data && data.record && typeof data.record === "string" && data.record.length > 0) { + console.log(`[SignalStorage] ✅ Loaded session for recipient ${recipientId} (address: ${encodedAddress})`); resolve(data.record); } else { // Try with number if string didn't work (backward compatibility) @@ -358,18 +359,19 @@ export class SignalProtocolStorage implements StorageType { numRequest.onsuccess = () => { const numData = numRequest.result; if (numData && numData.record && typeof numData.record === "string" && numData.record.length > 0) { + console.log(`[SignalStorage] ✅ Loaded session for recipient ${recipientId} (address: ${encodedAddress}, using number key)`); resolve(numData.record); } else { - console.warn(`Session record missing or invalid for recipient ${recipientId} (address: ${encodedAddress})`); + console.warn(`[SignalStorage] ⚠️ Session record missing or invalid for recipient ${recipientId} (address: ${encodedAddress}) - checked both string and number keys`); resolve(undefined); } }; numRequest.onerror = () => { - console.warn(`Session record missing for recipient ${recipientId} (address: ${encodedAddress})`); + console.warn(`[SignalStorage] ⚠️ Session record missing for recipient ${recipientId} (address: ${encodedAddress}) - IndexedDB error`); resolve(undefined); }; } else { - console.warn(`Session record missing or invalid for recipient ${recipientId} (address: ${encodedAddress})`); + console.warn(`[SignalStorage] ⚠️ Session record missing or invalid for recipient ${recipientId} (address: ${encodedAddress}) - no data found`); resolve(undefined); } } @@ -391,7 +393,7 @@ export class SignalProtocolStorage implements StorageType { // Validate record if (!record || typeof record !== "string" || record.length === 0) { - console.warn(`Invalid session record for address ${encodedAddress}`); + console.warn(`[SignalStorage] Invalid session record for address ${encodedAddress}`); return; } @@ -405,6 +407,7 @@ export class SignalProtocolStorage implements StorageType { record: record }); request.onsuccess = () => { + console.log(`[SignalStorage] ✅ Stored session for recipient ${recipientId} (address: ${encodedAddress}, record length: ${record.length})`); resolve(); // If session sync callback is set and we're not restoring, upload to server in background (non-blocking) // Do this AFTER resolve() to ensure storage completes even if sync fails