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;
|
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 {
|
||||||
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) {
|
} 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 publicKey = await api.chats.dm.fetchUserPublicKey(otherUserId, user.authToken!);
|
const plaintexts = await fetchMessagePlaintextsForRecipient(otherUserId);
|
||||||
if (publicKey) {
|
const cached = plaintexts.get(conv.lastMessage.id);
|
||||||
// Decrypt the last message
|
if (cached) {
|
||||||
const decryptedJson = await decryptDm(conv.lastMessage, conv.lastMessage.senderId);
|
const data = JSON.parse(cached) as DmEncryptedJSON;
|
||||||
const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON;
|
lastMessageContent = formatDMMessageContent(data.data.content, conv.lastMessage.senderId, user.currentUser!.id);
|
||||||
lastMessageContent = formatDMMessageContent(decryptedData.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) {
|
} 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;
|
||||||
@@ -276,23 +308,29 @@ export function useDM() {
|
|||||||
const conversations = await api.chats.dm.conversations(user.authToken);
|
const conversations = await api.chats.dm.conversations(user.authToken);
|
||||||
const userConversation = conversations.find(conv => conv.user.id === userId);
|
const userConversation = conversations.find(conv => conv.user.id === userId);
|
||||||
|
|
||||||
if (userConversation) {
|
if (userConversation) {
|
||||||
let lastMessageContent: string | undefined = undefined;
|
let lastMessageContent: string | undefined = undefined;
|
||||||
|
|
||||||
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 publicKey = await api.chats.dm.fetchUserPublicKey(otherUserId, user.authToken!);
|
const cached = plaintexts.get(userConversation.lastMessage.id);
|
||||||
if (publicKey) {
|
if (cached) {
|
||||||
// Decrypt the last message
|
const data = JSON.parse(cached) as DmEncryptedJSON;
|
||||||
const decryptedJson = await decryptDm(userConversation.lastMessage, userConversation.lastMessage.senderId);
|
lastMessageContent = formatDMMessageContent(data.data.content, userConversation.lastMessage.senderId, user.currentUser!.id);
|
||||||
const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON;
|
}
|
||||||
lastMessageContent = formatDMMessageContent(decryptedData.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) {
|
} 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 {
|
||||||
const publicKey = await api.chats.dm.fetchUserPublicKey(otherUserId, user.authToken!);
|
let messageContent: string | null = null;
|
||||||
if (publicKey) {
|
if (senderId === user.currentUser.id) {
|
||||||
const decryptedJson = await decryptDm(envelope, senderId);
|
const { fetchMessagePlaintextsForRecipient } = await import("@/utils/crypto/messagePlaintextSync");
|
||||||
const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON;
|
const plaintexts = await fetchMessagePlaintextsForRecipient(otherUserId);
|
||||||
const messageContent = decryptedData.data.content;
|
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);
|
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 {
|
||||||
const publicKey = await api.chats.dm.fetchUserPublicKey(otherUserId, user.authToken!);
|
let messageContent: string | null = null;
|
||||||
if (publicKey) {
|
if (senderId === user.currentUser.id) {
|
||||||
const decryptedJson = await decryptDm(envelope, senderId);
|
const { fetchMessagePlaintextsForRecipient } = await import("@/utils/crypto/messagePlaintextSync");
|
||||||
const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON;
|
const plaintexts = await fetchMessagePlaintextsForRecipient(otherUserId);
|
||||||
const messageContent = decryptedData.data.content;
|
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);
|
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) {
|
||||||
throw new Error("Cannot decrypt own sent message - plaintext must be fetched from server first");
|
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 {
|
} 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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.clearMessages();
|
// Only clear and replace if we actually decrypted something
|
||||||
decryptedMessages.forEach(msg => this.addMessage(msg));
|
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);
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
console.warn("Message plaintext sync not initialized (missing token/userId)");
|
let token = syncToken;
|
||||||
return plaintexts; // Return empty map if not initialized
|
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 {
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
const existingSession = await storage.loadSession(address);
|
// The server has the authoritative copy encrypted with password-derived key
|
||||||
if (existingSession) {
|
try {
|
||||||
console.log(`Skipping restoration for recipient ${sessionData.recipientId} - local session already exists`);
|
const existingSession = await storage.loadSession(address);
|
||||||
continue;
|
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);
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user