Simplify chat store for plaintext messaging
This commit is contained in:
+32
-75
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user