Try fixing initialization

This commit is contained in:
2025-12-04 12:34:34 +03:00
Unverified
parent c5d5cdf9d2
commit 5cdaab35f5
8 changed files with 315 additions and 74 deletions
+82 -23
View File
@@ -52,6 +52,10 @@ export function useDM() {
if (!user.authToken) return; if (!user.authToken) return;
try { try {
// Wait for session restoration to complete (if in progress)
const { waitForSessionRestore } = await import("@/utils/crypto/sessionRestoreState");
await waitForSessionRestore();
// Get public key // Get public key
const publicKey = await api.chats.dm.fetchUserPublicKey(dmUser.id, user.authToken); const publicKey = await api.chats.dm.fetchUserPublicKey(dmUser.id, user.authToken);
if (!publicKey) return; if (!publicKey) return;
@@ -63,11 +67,22 @@ export function useDM() {
// Find last message // Find last message
const lastMessage = messages[messages.length - 1]; const lastMessage = messages[messages.length - 1];
let lastPlaintext: string | null = null; let lastPlaintext: string | null = null;
const isAuthor = lastMessage.senderId === user.currentUser?.id;
try { try {
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; lastPlaintext = (JSON.parse(await decryptDm(lastMessage, lastMessage.senderId)) as DmEncryptedJSON).data.content;
}
} catch (error) { } catch (error) {
console.error("Failed to decrypt last message:", error); console.error("Failed to get last message preview:", error);
} }
// Calculate unread count // Calculate unread count
@@ -102,6 +117,10 @@ export function useDM() {
usersLoadedRef.current = true; usersLoadedRef.current = true;
setIsLoadingUsers(true); setIsLoadingUsers(true);
try { 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); const conversations = await api.chats.dm.conversations(user.authToken);
// Process conversations and decrypt last messages // Process conversations and decrypt last messages
@@ -111,17 +130,25 @@ export function useDM() {
if (conv.lastMessage) { if (conv.lastMessage) {
try { try {
// Get the public key for the other user const isAuthor = conv.lastMessage.senderId === user.currentUser?.id;
const otherUserId = conv.lastMessage.senderId === user.currentUser?.id const otherUserId = conv.user.id; // the other party in the conversation
? conv.lastMessage.recipientId if (isAuthor) {
: conv.lastMessage.senderId; // 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!); const publicKey = await api.chats.dm.fetchUserPublicKey(otherUserId, user.authToken!);
if (publicKey) { if (publicKey) {
// Decrypt the last message
const decryptedJson = await decryptDm(conv.lastMessage, conv.lastMessage.senderId); const decryptedJson = await decryptDm(conv.lastMessage, conv.lastMessage.senderId);
const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON; const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON;
lastMessageContent = formatDMMessageContent(decryptedData.data.content, conv.lastMessage.senderId, user.currentUser?.id!); lastMessageContent = formatDMMessageContent(decryptedData.data.content, conv.lastMessage.senderId, user.currentUser!.id);
}
} }
} catch (error) { } catch (error) {
// Silently fail for last message decryption - it's not critical // Silently fail for last message decryption - it's not critical
@@ -160,6 +187,11 @@ export function useDM() {
setIsLoadingHistory(true); setIsLoadingHistory(true);
try { 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 { messages } = await api.chats.dm.fetchMessages(userId, user.authToken, 50);
const decryptedMessages: Message[] = []; const decryptedMessages: Message[] = [];
let maxIncomingId = 0; let maxIncomingId = 0;
@@ -281,17 +313,23 @@ export function useDM() {
if (userConversation.lastMessage) { if (userConversation.lastMessage) {
try { try {
// Get the public key for the other user const isAuthor = userConversation.lastMessage.senderId === user.currentUser?.id;
const otherUserId = userConversation.lastMessage.senderId === user.currentUser?.id const otherUserId = userId;
? userConversation.lastMessage.recipientId if (isAuthor) {
: userConversation.lastMessage.senderId; 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!); const publicKey = await api.chats.dm.fetchUserPublicKey(otherUserId, user.authToken!);
if (publicKey) { if (publicKey) {
// Decrypt the last message
const decryptedJson = await decryptDm(userConversation.lastMessage, userConversation.lastMessage.senderId); const decryptedJson = await decryptDm(userConversation.lastMessage, userConversation.lastMessage.senderId);
const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON; const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON;
lastMessageContent = formatDMMessageContent(decryptedData.data.content, userConversation.lastMessage.senderId, user.currentUser?.id!); lastMessageContent = formatDMMessageContent(decryptedData.data.content, userConversation.lastMessage.senderId, user.currentUser!.id);
}
} }
} catch (error) { } catch (error) {
console.error("Failed to decrypt last message for user", userId, error); console.error("Failed to decrypt last message for user", userId, error);
@@ -338,20 +376,30 @@ export function useDM() {
// Update unread count and last message preview // Update unread count and last message preview
try { try {
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!); const publicKey = await api.chats.dm.fetchUserPublicKey(otherUserId, user.authToken!);
if (publicKey) { if (publicKey) {
const decryptedJson = await decryptDm(envelope, senderId); const decryptedJson = await decryptDm(envelope, senderId);
const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON; const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON;
const messageContent = decryptedData.data.content; messageContent = decryptedData.data.content;
}
}
if (messageContent !== null) {
const formattedMessage = formatDMMessageContent(messageContent, senderId, user.currentUser.id); const formattedMessage = formatDMMessageContent(messageContent, senderId, user.currentUser.id);
setDmUsersState(prev => prev.map(u => setDmUsersState(prev => prev.map(u =>
u.id === otherUserId u.id === otherUserId
? { ? {
...u, ...u,
unreadCount: senderId !== user.currentUser?.id ? u.unreadCount + 1 : u.unreadCount, unreadCount: senderId !== user.currentUser?.id ? u.unreadCount + 1 : u.unreadCount,
lastMessage: formattedMessage, lastMessage: formattedMessage
publicKey
} }
: u : u
)); ));
@@ -368,18 +416,29 @@ export function useDM() {
} }
const otherUserId = senderId === user.currentUser.id ? recipientId : senderId; const otherUserId = senderId === user.currentUser.id ? recipientId : senderId;
try { try {
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!); const publicKey = await api.chats.dm.fetchUserPublicKey(otherUserId, user.authToken!);
if (publicKey) { if (publicKey) {
const decryptedJson = await decryptDm(envelope, senderId); const decryptedJson = await decryptDm(envelope, senderId);
const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON; const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON;
const messageContent = decryptedData.data.content; messageContent = decryptedData.data.content;
}
}
if (messageContent !== null) {
const formattedMessage = formatDMMessageContent(messageContent, senderId, user.currentUser.id); const formattedMessage = formatDMMessageContent(messageContent, senderId, user.currentUser.id);
setDmUsersState(prev => prev.map(u => setDmUsersState(prev => prev.map(u =>
u.id === otherUserId u.id === otherUserId
? { ? {
...u, ...u,
lastMessage: formattedMessage, lastMessage: formattedMessage
publicKey
} }
: u : u
)); ));
@@ -63,7 +63,7 @@ export class DMPanel extends MessagePanel {
this.failedDecryptionIds.clear(); 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 // Check if this is a message sent by the current user
const isSentByUs = env.senderId === this.currentUser.currentUser?.id; const isSentByUs = env.senderId === this.currentUser.currentUser?.id;
@@ -71,8 +71,24 @@ export class DMPanel extends MessagePanel {
if (isSentByUs) { if (isSentByUs) {
// Can't decrypt our own sent messages in Signal Protocol // Can't decrypt our own sent messages in Signal Protocol
// The plaintext should be passed in from loadMessages (fetched from server) // 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 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"); throw new Error("Cannot decrypt own sent message - plaintext must be fetched from server first");
}
}
} else { } else {
// Decrypt incoming messages // Decrypt incoming messages
plaintext = await decryptDm(env, env.senderId); plaintext = await decryptDm(env, env.senderId);
@@ -125,6 +141,11 @@ export class DMPanel extends MessagePanel {
this.setLoading(true); this.setLoading(true);
try { 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 // Ensure Signal Protocol session is established before fetching messages
if (!this.signalService && this.currentUser.currentUser?.id) { if (!this.signalService && this.currentUser.currentUser?.id) {
this.signalService = new SignalProtocolService(this.currentUser.currentUser.id.toString()); 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); 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 // 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 {
} }
} }
// Only clear and replace if we actually decrypted something
if (decryptedMessages.length > 0) {
this.clearMessages(); this.clearMessages();
decryptedMessages.forEach(msg => this.addMessage(msg)); 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); this.setHasMoreMessages(has_more);
// Update last read ID // Update last read ID
@@ -434,23 +462,56 @@ export class DMPanel extends MessagePanel {
// Mark as processed before attempting decryption // Mark as processed before attempting decryption
this.processedMessageIds.add(envelope.id); this.processedMessageIds.add(envelope.id);
const dmMsg = await this.parseTextPayload(envelope, this.getMessages());
// Check if this is a confirmation of a message we sent // 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) { if (isOurMessage) {
// This is our message being confirmed, find the temp message and replace it // 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); const tempMessages = this.getMessages().filter(m => m.id === -1 && m.runtimeData?.sendingState?.tempId);
let tempMsgContent: string | null = null; let tempMsgContent: string | null = null;
for (const tempMsg of tempMessages) { 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 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 // Upload the plaintext to server (encrypted) so we can display it in history
const { uploadMessagePlaintext } = await import("@/utils/crypto/messagePlaintextSync"); const { uploadMessagePlaintext } = await import("@/utils/crypto/messagePlaintextSync");
if (tempMsgContent) { if (tempMsgContent) {
await uploadMessagePlaintext(envelope.id, this.dmData.userId, tempMsgContent); await uploadMessagePlaintext(this.dmData.userId, envelope.id, tempMsgContent);
} }
return; return;
} }
@@ -460,10 +521,17 @@ export class DMPanel extends MessagePanel {
// (this might happen if the page was reloaded) // (this might happen if the page was reloaded)
if (!tempMsgContent && dmMsg.content) { if (!tempMsgContent && dmMsg.content) {
const { uploadMessagePlaintext } = await import("@/utils/crypto/messagePlaintextSync"); 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); this.addMessage(dmMsg);
// Update last read if it's from the other user // Update last read if it's from the other user
+15 -1
View File
@@ -9,6 +9,7 @@ import { typingManager } from "@/core/typingManager";
import type { UserState } from "./types"; import type { UserState } from "./types";
import { clearSessionSync } from "@/utils/crypto/sessionSync"; import { clearSessionSync } from "@/utils/crypto/sessionSync";
import { clearMessagePlaintextSync } from "@/utils/crypto/messagePlaintextSync"; import { clearMessagePlaintextSync } from "@/utils/crypto/messagePlaintextSync";
import { resetSessionRestoreState } from "@/utils/crypto/sessionRestoreState";
interface UserStore { interface UserStore {
user: UserState; user: UserState;
@@ -66,6 +67,13 @@ export const useUserStore = create<UserStore>((set) => ({
clearSessionSync(); clearSessionSync();
clearMessagePlaintextSync(); clearMessagePlaintextSync();
// Reset session restore state
try {
resetSessionRestoreState();
} catch (error) {
console.error("Failed to reset session restore state:", error);
}
set({ set({
user: { user: {
currentUser: null, currentUser: null,
@@ -132,14 +140,20 @@ export const useUserStore = create<UserStore>((set) => ({
if (storedKey) { if (storedKey) {
console.log("[RestoreFromStorage] Stored session key found, restoring sessions from server..."); console.log("[RestoreFromStorage] Stored session key found, restoring sessions from server...");
// We can restore sessions using the stored key (password not needed) // 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 { try {
await restoreSessionsFromServer(user.id.toString(), null, token); await restorePromise;
console.log("[RestoreFromStorage] Sessions restored from server using stored key"); console.log("[RestoreFromStorage] Sessions restored from server using stored key");
} catch (error) { } catch (error) {
console.error("[RestoreFromStorage] Failed to restore sessions:", error); console.error("[RestoreFromStorage] Failed to restore sessions:", error);
} }
} else { } else {
console.warn("[RestoreFromStorage] No stored session key - user needs to log in to derive key"); 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 // Re-upload prekeys to ensure they are fresh
@@ -4,7 +4,7 @@
*/ */
import { encryptMessagePlaintext, decryptMessagePlaintext } from "./messagePlaintextEncryption"; 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 // Global state for message plaintext sync
let syncPassword: string | null = null; let syncPassword: string | null = null;
@@ -75,21 +75,42 @@ export async function fetchMessagePlaintextsForRecipient(
): Promise<Map<number, string>> { ): Promise<Map<number, string>> {
const plaintexts = new Map<number, string>(); const plaintexts = new Map<number, string>();
if (!syncToken || !syncUserId) { // 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)"); console.warn("Message plaintext sync not initialized (missing token/userId)");
return plaintexts; // Return empty map if not initialized 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 { try {
console.log(`Fetching encrypted plaintexts for recipient ${recipientId}...`); 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...`); console.log(`Found ${encryptedMessages.length} encrypted plaintexts, decrypting...`);
for (const msg of encryptedMessages) { for (const msg of encryptedMessages) {
try { try {
// Use stored key if available, otherwise use password to derive it // 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); plaintexts.set(msg.messageId, plaintext);
} catch (error) { } catch (error) {
console.warn(`Failed to decrypt plaintext for message ${msg.messageId}:`, error); console.warn(`Failed to decrypt plaintext for message ${msg.messageId}:`, error);
@@ -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<void> | null = null;
let restoreComplete = false;
/**
* Mark that session restoration has started
*/
export function setRestoringSessions(promise: Promise<void>): 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<void> {
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;
}
+10 -5
View File
@@ -110,14 +110,19 @@ export async function restoreSessionsFromServer(
try { try {
const address = `${sessionData.recipientId}.${sessionData.deviceId}`; const address = `${sessionData.recipientId}.${sessionData.deviceId}`;
// Check if we already have a local session - if so, skip restoration // Always restore from server to ensure we have a valid session
// This prevents overwriting a newer session with an older one // 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); const existingSession = await storage.loadSession(address);
if (existingSession) { if (existingSession) {
console.log(`Skipping restoration for recipient ${sessionData.recipientId} - local session already exists`); console.log(`[Session Sync] Local session exists for recipient ${sessionData.recipientId}, but restoring from server to ensure validity`);
continue; }
} 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); const encryptedBlob = decodeSessionBlob(sessionData.encryptedData);
// Use stored key if available, otherwise use password to derive it // Use stored key if available, otherwise use password to derive it
const sessionRecord = await decryptSessionWithPassword(password, userId, encryptedBlob); 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) // Store in IndexedDB (sync callback won't fire because isRestoring is true)
await storage.storeSession(address, sessionRecord); await storage.storeSession(address, sessionRecord);
restoredCount++; restoredCount++;
console.log(`Restored session for recipient ${sessionData.recipientId}`); console.log(`[Session Sync ✅] Restored session for recipient ${sessionData.recipientId} from server`);
} catch (error) { } catch (error) {
failedCount++; failedCount++;
console.warn(`Failed to restore session for recipient ${sessionData.recipientId}:`, error); console.warn(`Failed to restore session for recipient ${sessionData.recipientId}:`, error);
@@ -73,8 +73,11 @@ export async function initializeSignalProtocol({
console.log("========================================"); console.log("========================================");
console.log("[Signal Protocol Init] Step 5: ⚠️ RESTORING SESSIONS FROM SERVER"); console.log("[Signal Protocol Init] Step 5: ⚠️ RESTORING SESSIONS FROM SERVER");
console.log("========================================"); console.log("========================================");
const { setRestoringSessions } = await import("./sessionRestoreState");
const restorePromise = restoreSessionsFromServer(userId, password, token);
setRestoringSessions(restorePromise);
try { try {
await restoreSessionsFromServer(userId, password, token); await restorePromise;
console.log("========================================"); console.log("========================================");
console.log("[Signal Protocol Init] Step 5: ✅ SESSIONS RESTORED FROM SERVER"); console.log("[Signal Protocol Init] Step 5: ✅ SESSIONS RESTORED FROM SERVER");
console.log("========================================"); console.log("========================================");
@@ -85,6 +88,10 @@ export async function initializeSignalProtocol({
console.error("========================================"); console.error("========================================");
// Continue even if restoration fails // 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 // Step 6: Upload prekey bundle
+7 -4
View File
@@ -350,6 +350,7 @@ export class SignalProtocolStorage implements StorageType {
request.onsuccess = () => { request.onsuccess = () => {
const data = request.result; const data = request.result;
if (data && data.record && typeof data.record === "string" && data.record.length > 0) { 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); resolve(data.record);
} else { } else {
// Try with number if string didn't work (backward compatibility) // Try with number if string didn't work (backward compatibility)
@@ -358,18 +359,19 @@ export class SignalProtocolStorage implements StorageType {
numRequest.onsuccess = () => { numRequest.onsuccess = () => {
const numData = numRequest.result; const numData = numRequest.result;
if (numData && numData.record && typeof numData.record === "string" && numData.record.length > 0) { 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); resolve(numData.record);
} else { } 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); resolve(undefined);
} }
}; };
numRequest.onerror = () => { 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); resolve(undefined);
}; };
} else { } 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); resolve(undefined);
} }
} }
@@ -391,7 +393,7 @@ export class SignalProtocolStorage implements StorageType {
// Validate record // Validate record
if (!record || typeof record !== "string" || record.length === 0) { 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; return;
} }
@@ -405,6 +407,7 @@ export class SignalProtocolStorage implements StorageType {
record: record record: record
}); });
request.onsuccess = () => { request.onsuccess = () => {
console.log(`[SignalStorage] ✅ Stored session for recipient ${recipientId} (address: ${encodedAddress}, record length: ${record.length})`);
resolve(); resolve();
// If session sync callback is set and we're not restoring, upload to server in background (non-blocking) // 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 // Do this AFTER resolve() to ensure storage completes even if sync fails