mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Try fixing initialization
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -338,20 +376,30 @@ export function useDM() {
|
||||
|
||||
// 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
|
||||
));
|
||||
|
||||
@@ -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,10 +521,17 @@ 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);
|
||||
|
||||
// Update last read if it's from the other user
|
||||
|
||||
@@ -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;
|
||||
@@ -66,6 +67,13 @@ export const useUserStore = create<UserStore>((set) => ({
|
||||
clearSessionSync();
|
||||
clearMessagePlaintextSync();
|
||||
|
||||
// Reset session restore state
|
||||
try {
|
||||
resetSessionRestoreState();
|
||||
} catch (error) {
|
||||
console.error("Failed to reset session restore state:", error);
|
||||
}
|
||||
|
||||
set({
|
||||
user: {
|
||||
currentUser: null,
|
||||
@@ -132,14 +140,20 @@ export const useUserStore = create<UserStore>((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
|
||||
|
||||
@@ -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<Map<number, string>> {
|
||||
const plaintexts = new Map<number, string>();
|
||||
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user