Simplify chat store for plaintext messaging

This commit is contained in:
2026-03-11 21:15:18 -07:00
parent b6444d1426
commit 25a63b0de8
+32 -75
View File
@@ -2,21 +2,11 @@ import { create } from 'zustand';
import { io, Socket } from 'socket.io-client';
import { getWsUrl } from '@/lib/ws';
import * as chatApi from '@/api/chat';
import * as cryptoLib from '@/lib/crypto';
import type { ConversationEntry } from '@/api/chat';
interface DecryptedMessage {
id: string;
senderId: string;
recipientId: string;
content: string;
decryptionFailed: boolean;
createdAt: string;
}
import type { ChatMessage, ConversationEntry } from '@/api/chat';
interface ChatState {
conversations: ConversationEntry[];
messages: DecryptedMessage[];
messages: ChatMessage[];
socket: Socket | null;
typingFriendIds: Set<string>;
activeFriendId: string | null;
@@ -29,7 +19,7 @@ interface ChatState {
fetchConversations: () => Promise<void>;
openConversation: (friendId: string) => Promise<void>;
loadMoreMessages: () => Promise<void>;
sendMessage: (friendId: string, plaintext: string) => Promise<void>;
sendMessage: (friendId: string, content: string) => void;
clearChat: () => void;
}
@@ -55,13 +45,18 @@ export const useChatStore = create<ChatState>((set, get) => ({
transports: ['websocket'],
});
socket.on('new-message', async (msg) => {
const { activeFriendId } = get();
if (msg.senderId === activeFriendId || msg.recipientId === activeFriendId) {
const decrypted = await decryptMessage(msg);
set((state) => ({
messages: [...state.messages, decrypted],
}));
socket.on('new-message', (msg: ChatMessage) => {
const { activeFriendId, messages } = get();
if (
msg.senderId === activeFriendId ||
msg.recipientId === activeFriendId
) {
// Avoid duplicates (sender gets it back from server)
if (!messages.some((m) => m.id === msg.id)) {
set((state) => ({
messages: [...state.messages, msg],
}));
}
}
});
@@ -71,7 +66,6 @@ export const useChatStore = create<ChatState>((set, get) => ({
next.add(data.senderId);
return { typingFriendIds: next };
});
// Clear typing indicator after 3 seconds
setTimeout(() => {
set((state) => {
const next = new Set(state.typingFriendIds);
@@ -102,12 +96,17 @@ export const useChatStore = create<ChatState>((set, get) => ({
},
openConversation: async (friendId: string) => {
set({ activeFriendId: friendId, messages: [], loading: true, hasMore: false, nextCursor: null });
set({
activeFriendId: friendId,
messages: [],
loading: true,
hasMore: false,
nextCursor: null,
});
try {
const result = await chatApi.getMessages(friendId);
const decrypted = await Promise.all(result.messages.map(decryptMessage));
set({
messages: decrypted,
messages: result.messages,
hasMore: result.hasMore,
nextCursor: result.nextCursor,
loading: false,
@@ -124,9 +123,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
set({ loading: true });
try {
const result = await chatApi.getMessages(activeFriendId, nextCursor);
const decrypted = await Promise.all(result.messages.map(decryptMessage));
set((state) => ({
messages: [...decrypted, ...state.messages],
messages: [...result.messages, ...state.messages],
hasMore: result.hasMore,
nextCursor: result.nextCursor,
loading: false,
@@ -136,59 +134,18 @@ export const useChatStore = create<ChatState>((set, get) => ({
}
},
sendMessage: async (friendId: string, plaintext: string) => {
sendMessage: (friendId: string, content: string) => {
const { socket } = get();
if (!socket) return;
try {
const encrypted = await cryptoLib.encryptForFriend(friendId, plaintext);
socket.emit('send-message', {
recipientId: friendId,
encryptedContent: encrypted.ciphertext,
iv: encrypted.iv,
});
} catch (err) {
console.error('Failed to encrypt/send message:', err);
throw err;
}
socket.emit('send-message', { recipientId: friendId, content });
},
clearChat: () => {
set({ messages: [], activeFriendId: null, hasMore: false, nextCursor: null });
set({
messages: [],
activeFriendId: null,
hasMore: false,
nextCursor: null,
});
},
}));
async function decryptMessage(msg: {
id: string;
senderId: string;
recipientId: string;
encryptedContent: string;
iv: string;
createdAt: string;
}): Promise<DecryptedMessage> {
try {
// Determine the other party (the one whose public key we need for shared secret)
const content = await cryptoLib.decryptFromFriend(
msg.senderId,
msg.encryptedContent,
msg.iv,
);
return {
id: msg.id,
senderId: msg.senderId,
recipientId: msg.recipientId,
content,
decryptionFailed: false,
createdAt: msg.createdAt,
};
} catch {
return {
id: msg.id,
senderId: msg.senderId,
recipientId: msg.recipientId,
content: '',
decryptionFailed: true,
createdAt: msg.createdAt,
};
}
}