From a92f91d79189399e526b114af7fb744ab24cb34d Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Fri, 17 Oct 2025 15:52:12 +0300 Subject: [PATCH] Implement live last message update --- .cursor/rules/general.mdc | 1 + backend/routes/messaging.py | 2 + frontend/src/core/api/dmApi.ts | 8 +- frontend/src/core/types.d.ts | 3 + frontend/src/pages/chat/css/_left-panel.scss | 9 + frontend/src/pages/chat/hooks/useDM.ts | 255 +++++++++++----- .../src/pages/chat/ui/left/BottomAppBar.tsx | 0 frontend/src/pages/chat/ui/left/ChatTabs.tsx | 43 +-- .../src/pages/chat/ui/left/DMUsersList.tsx | 80 ------ frontend/src/pages/chat/ui/left/LeftPanel.tsx | 79 +---- .../pages/chat/ui/left/UnifiedChatsList.tsx | 272 ++++++++++++++++++ .../src/pages/chat/ui/left/UsernameSearch.tsx | 4 +- .../src/pages/chat/ui/right/ChatMessages.tsx | 10 +- .../src/pages/chat/ui/right/panels/DMPanel.ts | 10 +- 14 files changed, 518 insertions(+), 258 deletions(-) delete mode 100644 frontend/src/pages/chat/ui/left/BottomAppBar.tsx delete mode 100644 frontend/src/pages/chat/ui/left/DMUsersList.tsx create mode 100644 frontend/src/pages/chat/ui/left/UnifiedChatsList.tsx diff --git a/.cursor/rules/general.mdc b/.cursor/rules/general.mdc index 0eefb7f..8504a0a 100644 --- a/.cursor/rules/general.mdc +++ b/.cursor/rules/general.mdc @@ -16,6 +16,7 @@ When working with this project, follow these rules: - Use double quotes ("") for strings consistently. - Prefer functional components over class components in React. - Use TypeScript strictly - avoid `any` types unless absolutely necessary. +- DO NOT leave placeholders - ask me when it would be better or implement it fully. ## File Operations - If possible, try to update files in a single edit when making multiple changes. diff --git a/backend/routes/messaging.py b/backend/routes/messaging.py index dc976d6..d24dacc 100644 --- a/backend/routes/messaging.py +++ b/backend/routes/messaging.py @@ -835,6 +835,8 @@ class MessaggingSocketManager: "type": "dmEdited", "data": { "id": env.id, + "senderId": env.sender_id, + "recipientId": env.recipient_id, "iv": env.iv_b64, "ciphertext": env.ciphertext_b64, "iv2": env.iv2_b64, diff --git a/frontend/src/core/api/dmApi.ts b/frontend/src/core/api/dmApi.ts index 4b3bd71..e2003d8 100644 --- a/frontend/src/core/api/dmApi.ts +++ b/frontend/src/core/api/dmApi.ts @@ -170,7 +170,13 @@ export async function deleteDmEnvelope(id: number, recipientId: number, authToke }); } -export async function fetchDMConversations(token: string): Promise { +export interface DMConversationResponse { + user: User; + lastMessage: DmEnvelope; + unreadCount: number; +} + +export async function fetchDMConversations(token: string): Promise { const res = await fetch(`${API_BASE_URL}/dm/conversations`, { headers: getAuthHeaders(token, true) }); diff --git a/frontend/src/core/types.d.ts b/frontend/src/core/types.d.ts index 940ba78..6b9382c 100644 --- a/frontend/src/core/types.d.ts +++ b/frontend/src/core/types.d.ts @@ -305,11 +305,14 @@ export interface Attachment { // Utils export interface DMEditPayload { id: number; + senderId: number; + recipientId: number; iv: string; ciphertext: string; iv2: string; wrappedMk: string; salt: string; + timestamp: string; } // Requests diff --git a/frontend/src/pages/chat/css/_left-panel.scss b/frontend/src/pages/chat/css/_left-panel.scss index 27add66..4f4eced 100644 --- a/frontend/src/pages/chat/css/_left-panel.scss +++ b/frontend/src/pages/chat/css/_left-panel.scss @@ -239,3 +239,12 @@ } } } + +// Description styling for list items +.list-description { + word-wrap: break-word; + overflow: hidden; + display: -webkit-box; + line-clamp: 2; + -webkit-box-orient: vertical; +} diff --git a/frontend/src/pages/chat/hooks/useDM.ts b/frontend/src/pages/chat/hooks/useDM.ts index 989aa95..9e4b36d 100644 --- a/frontend/src/pages/chat/hooks/useDM.ts +++ b/frontend/src/pages/chat/hooks/useDM.ts @@ -5,7 +5,8 @@ import { fetchDMHistory, decryptDm, sendDMViaWebSocket, - fetchDMConversations + fetchDMConversations, + type DMConversationResponse } from "@/core/api/dmApi"; import type { User, Message, DmEncryptedJSON } from "@/core/types"; import { websocket } from "@/core/websocket"; @@ -16,8 +17,34 @@ export interface DMUser extends User { publicKey?: string | null; } +// Utility function for consistent username formatting in DM messages +export function formatDMUsername( + senderId: number, + _recipientId: number, + currentUserId: number, + otherUsername: string +): string { + const isFromCurrentUser = senderId === currentUserId; + return isFromCurrentUser ? "Вы" : otherUsername; +} + +// Utility function for consistent message content formatting +export function formatDMMessageContent( + content: string, + senderId: number, + currentUserId: number +): string { + const isFromCurrentUser = senderId === currentUserId; + const prefix = isFromCurrentUser ? "Вы: " : ""; + const maxContentLength = 50 - prefix.length; + const truncatedContent = content.length > maxContentLength + ? content.substring(0, maxContentLength) + "..." + : content; + return prefix + truncatedContent; +} + export function useDM() { - const { user, chat, setDmUsers, setActiveDm, addMessage, clearMessages } = useAppState(); + const { user, setDmUsers, setActiveDm, addMessage, clearMessages } = useAppState(); const [dmUsers, setDmUsersState] = useState([]); const [isLoadingUsers, setIsLoadingUsers] = useState(false); const [isLoadingHistory, setIsLoadingHistory] = useState(false); @@ -80,17 +107,42 @@ export function useDM() { setIsLoadingUsers(true); try { const conversations = await fetchDMConversations(user.authToken); - console.log("Fetched conversations:", conversations); - const dmUsersWithState: DMUser[] = conversations.map((conv: any) => ({ - ...conv.user, - unreadCount: conv.unreadCount, - lastMessage: conv.lastMessage ? "Последнее сообщение" : undefined, - publicKey: null - })); + // Process conversations and decrypt last messages + const dmUsersWithState: DMUser[] = await Promise.all( + conversations.map(async (conv: DMConversationResponse) => { + let lastMessageContent: string | undefined = undefined; + + 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 fetchUserPublicKey(otherUserId, user.authToken!); + if (publicKey) { + // Decrypt the last message + const decryptedJson = await decryptDm(conv.lastMessage, publicKey!); + const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON; + lastMessageContent = formatDMMessageContent(decryptedData.data.content, conv.lastMessage.senderId, user.currentUser?.id!); + } + } catch (error) { + console.error("Failed to decrypt last message for user", conv.user.id, error); + } + } + + return { + ...conv.user, + unreadCount: conv.unreadCount, + lastMessage: lastMessageContent, + publicKey: null + }; + }) + ); setDmUsersState(dmUsersWithState); - setDmUsers(conversations.map((conv: any) => conv.user)); + setDmUsers(conversations.map((conv: DMConversationResponse) => conv.user)); } catch (error) { console.error("Failed to load DM conversations:", error); @@ -192,7 +244,67 @@ export function useDM() { } }, [user.authToken, setActiveDm, loadDMHistory]); - // WebSocket message handler + // Force reload users (useful for refreshing the list) + const reloadUsers = useCallback(() => { + usersLoadedRef.current = false; + loadUsers(); + }, [loadUsers]); + + // Reload a specific user's conversation data + const reloadUserConversation = useCallback(async (userId: number) => { + if (!user.authToken) return; + + try { + const conversations = await fetchDMConversations(user.authToken); + const userConversation = conversations.find(conv => conv.user.id === userId); + + 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 fetchUserPublicKey(otherUserId, user.authToken!); + if (publicKey) { + // Decrypt the last message + const decryptedJson = await decryptDm(userConversation.lastMessage, publicKey!); + 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); + } + } + + // Update the specific user in the state + setDmUsersState(prev => prev.map(u => { + if (u.id === userId) { + return { + ...u, + lastMessage: lastMessageContent, + unreadCount: userConversation.unreadCount + }; + } + return u; + })); + } else { + // If conversation no longer exists, remove the user from the list + setDmUsersState(prev => prev.filter(u => u.id !== userId)); + // Get current dmUsers and filter out the removed user + const currentDmUsers = useAppState.getState().chat.dmUsers; + setDmUsers(currentDmUsers.filter((u: User) => u.id !== userId)); + } + } catch (error) { + console.error("Failed to reload user conversation:", error); + } + }, [user.authToken]); + + + // WebSocket message handler for conversation list updates useEffect(() => { async function handleWebSocketMessage(e: MessageEvent) { try { @@ -200,56 +312,70 @@ export function useDM() { if (msg.type === "dmNew") { const { senderId, recipientId, ...envelope } = msg.data; - // If this is for the active DM conversation - if (chat.activeDm && (senderId === chat.activeDm.userId || recipientId === chat.activeDm.userId)) { - try { - const plaintext = await decryptDm(envelope, chat.activeDm.publicKey!); - const isAuthor = senderId !== chat.activeDm.userId; - - addMessage({ - id: envelope.id, - content: plaintext, - username: isAuthor ? (user.currentUser?.username || "Unknown") : (chat.activeDm.username || "Unknown"), - timestamp: envelope.timestamp, - is_read: false, - is_edited: false - }); - - // Update last read if it's from the other user - if (senderId === chat.activeDm.userId) { - setLastReadId(chat.activeDm.userId, Math.max(getLastReadId(chat.activeDm.userId), envelope.id)); - } - } catch (error) { - console.error("Failed to decrypt incoming DM:", error); - } - } else { - // Update unread count for other users - const otherUserId = senderId; - setDmUsersState(prev => prev.map(u => - u.id === otherUserId - ? { ...u, unreadCount: u.unreadCount + 1 } - : u - )); - - // Update last message preview - try { - const publicKey = await fetchUserPublicKey(otherUserId, user.authToken!); - if (publicKey) { - const plaintext = await decryptDm(envelope, publicKey); - setDmUsersState(prev => prev.map(u => - u.id === otherUserId - ? { - ...u, - lastMessage: plaintext.split(/\r?\n/).slice(0, 2).join("\n"), - publicKey - } - : u - )); - } - } catch (error) { - console.error("Failed to update last message preview:", error); - } + // Update conversation list (not active conversation - that's handled by DMPanel) + if (!user.currentUser?.id) { + return; } + const otherUserId = senderId === user.currentUser.id ? recipientId : senderId; + + // Update unread count and last message preview + try { + const publicKey = await fetchUserPublicKey(otherUserId, user.authToken!); + if (publicKey) { + const decryptedJson = await decryptDm(envelope, publicKey); + const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON; + const messageContent = decryptedData.data.content; + 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 + } + : u + )); + } + } catch (error) { + console.error("Failed to update last message preview:", error); + } + } else if (msg.type === "dmEdited") { + const { id, senderId, recipientId, ...envelope } = msg.data; + + // Update last message preview for conversation list + if (!user.currentUser?.id) { + return; + } + const otherUserId = senderId === user.currentUser.id ? recipientId : senderId; + try { + const publicKey = await fetchUserPublicKey(otherUserId, user.authToken!); + if (publicKey) { + const decryptedJson = await decryptDm(envelope, publicKey); + const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON; + const messageContent = decryptedData.data.content; + const formattedMessage = formatDMMessageContent(messageContent, senderId, user.currentUser.id); + setDmUsersState(prev => prev.map(u => + u.id === otherUserId + ? { + ...u, + lastMessage: formattedMessage, + publicKey + } + : u + )); + } + } catch (error) { + console.error("Failed to update edited message preview:", error); + } + } else if (msg.type === "dmDeleted") { + const { senderId, recipientId } = msg.data; + + // Reload only the specific user's conversation + if (!user.currentUser?.id) return; + const otherUserId = senderId === user.currentUser.id ? recipientId : senderId; + reloadUserConversation(otherUserId); } } catch (error) { console.error("Failed to handle WebSocket message:", error); @@ -259,13 +385,7 @@ export function useDM() { websocket.addEventListener("message", handleWebSocketMessage); return () => websocket.removeEventListener("message", handleWebSocketMessage); - }, [chat.activeDm, user.currentUser, addMessage]); - - // Force reload users (useful for refreshing the list) - const reloadUsers = useCallback(() => { - usersLoadedRef.current = false; - loadUsers(); - }, [loadUsers]); + }, [user.currentUser, user.authToken, reloadUserConversation]); return { dmUsers, @@ -273,6 +393,7 @@ export function useDM() { isLoadingHistory, loadUsers, reloadUsers, + reloadUserConversation, startDMConversation, sendDMMessage, loadUserLastMessage diff --git a/frontend/src/pages/chat/ui/left/BottomAppBar.tsx b/frontend/src/pages/chat/ui/left/BottomAppBar.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/pages/chat/ui/left/ChatTabs.tsx b/frontend/src/pages/chat/ui/left/ChatTabs.tsx index bf5bb8f..01f6460 100644 --- a/frontend/src/pages/chat/ui/left/ChatTabs.tsx +++ b/frontend/src/pages/chat/ui/left/ChatTabs.tsx @@ -1,11 +1,21 @@ -import { useAppState } from "@/pages/chat/state"; +import { useAppState, type ChatTabs } from "@/pages/chat/state"; +import { UnifiedChatsList } from "./UnifiedChatsList"; +import type { FormEvent } from "react"; +import type { Tabs } from "mdui/components/tabs"; export function ChatTabs() { - const { chat, setActiveTab, switchToPublicChat } = useAppState(); + const { chat, setActiveTab } = useAppState(); + + function handleChange(e: FormEvent & CustomEvent<{ value: string }>) { + setActiveTab(e.detail.value as ChatTabs); + } return (
- setActiveTab(e.detail.value)}> + Чаты @@ -15,37 +25,12 @@ export function ChatTabs() { Контакты - - ЛС - - - await switchToPublicChat("Общий чат")} - style={{ cursor: "pointer" }} - > - - - await switchToPublicChat("Общий чат 2")} - style={{ cursor: "pointer" }} - > - - - + Скоро будет... Скоро будет... - - -
); diff --git a/frontend/src/pages/chat/ui/left/DMUsersList.tsx b/frontend/src/pages/chat/ui/left/DMUsersList.tsx deleted file mode 100644 index 04ce84a..0000000 --- a/frontend/src/pages/chat/ui/left/DMUsersList.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useEffect } from "react"; -import { useDM, type DMUser } from "@/pages/chat/hooks/useDM"; -import { useAppState } from "@/pages/chat/state"; -import { fetchUserPublicKey } from "@/core/api/dmApi"; -import defaultAvatar from "@/images/default-avatar.png"; - -export function DMUsersList() { - const { dmUsers, isLoadingUsers, loadUsers } = useDM(); - const { chat, switchToDM } = useAppState(); - - useEffect(() => { - if (chat.activeTab === "chats") { - loadUsers(); - } - }, [chat.activeTab, loadUsers]); - - if (isLoadingUsers) { - return ( - - ); - } - - async function handleUserClick(user: DMUser) { - if (!user.publicKey) { - // Get public key if not already loaded - const authToken = useAppState.getState().user.authToken; - if (!authToken) return; - - const publicKey = await fetchUserPublicKey(user.id, authToken); - if (publicKey) { - user.publicKey = publicKey; - } else { - console.error("Failed to get public key for user:", user.id); - return; - } - } - - await switchToDM({ - userId: user.id, - username: user.username, - publicKey: user.publicKey, - profilePicture: user.profile_picture, - online: user.online || false - }); - }; - - return ( - - {dmUsers.map((user: DMUser) => ( - handleUserClick(user)} - style={{ cursor: "pointer" }} - > - {user.username} { - (e.target as HTMLImageElement).src = defaultAvatar; - }} - /> - {user.unreadCount > 0 && ( - - {user.unreadCount} - - )} - - ))} - - ); -} diff --git a/frontend/src/pages/chat/ui/left/LeftPanel.tsx b/frontend/src/pages/chat/ui/left/LeftPanel.tsx index 8340d82..47401bf 100644 --- a/frontend/src/pages/chat/ui/left/LeftPanel.tsx +++ b/frontend/src/pages/chat/ui/left/LeftPanel.tsx @@ -1,13 +1,9 @@ -import { PRODUCT_NAME } from "@/core/config"; import { useAppState } from "@/pages/chat/state"; -import defaultAvatar from "@/images/default-avatar.png"; -import { useState, type FormEvent } from "react"; -import { ProfileDialog } from "./profile/ProfileDialog"; +import { useState } from "react"; import { SettingsDialog } from "./settings/SettingsDialog"; -import { DMUsersList } from "./DMUsersList"; import { UsernameSearch } from "./UsernameSearch"; -import type { Tabs } from "mdui"; -import type { ChatTabs } from "@/pages/chat/state"; +import { ChatTabs } from "./ChatTabs"; +import { ChatHeader } from "./ChatHeader"; function BottomAppBar() { const [settingsOpen, onSettingsOpenChange] = useState(false); @@ -36,75 +32,6 @@ function BottomAppBar() { ); } - -function ChatTabs() { - const { chat, setActiveTab, switchToPublicChat } = useAppState(); - const { activeTab } = chat; - - async function handleChatClick(chatName: string) { - await switchToPublicChat(chatName); - } - - function handleTabChange(e: FormEvent) { - setActiveTab((e.target as Tabs).value as ChatTabs); - } - - return ( -
- - Чаты - Каналы - Контакты - - - - handleChatClick("Общий чат")} - style={{ cursor: "pointer" }} - > - - - handleChatClick("Общий чат 2")} - style={{ cursor: "pointer" }} - > - - - - {/* DM conversations will be loaded here */} - - - - Скоро будет... - Скоро будет... - -
- ); -} - - -function ChatHeader() { - const [isProfileOpen, setProfileOpen] = useState(false); - - return ( -
-
{PRODUCT_NAME}
- - -
- ); -} - export function LeftPanel() { return (
diff --git a/frontend/src/pages/chat/ui/left/UnifiedChatsList.tsx b/frontend/src/pages/chat/ui/left/UnifiedChatsList.tsx new file mode 100644 index 0000000..7d4ea27 --- /dev/null +++ b/frontend/src/pages/chat/ui/left/UnifiedChatsList.tsx @@ -0,0 +1,272 @@ +import { useState, useEffect, useCallback } from "react"; +import { useAppState } from "@/pages/chat/state"; +import { useDM, type DMUser } from "@/pages/chat/hooks/useDM"; +import { API_BASE_URL } from "@/core/config"; +import { getAuthHeaders } from "@/core/api/authApi"; +import { fetchUserPublicKey } from "@/core/api/dmApi"; +import type { Message } from "@/core/types"; +import { websocket } from "@/core/websocket"; +import defaultAvatar from "@/images/default-avatar.png"; + +interface PublicChat { + id: string; + name: string; + type: "public"; + lastMessage?: Message; +} + +interface DMConversation { + id: number; + username: string; + profile_picture?: string; + online?: boolean; + type: "dm"; + lastMessage?: string; + unreadCount: number; + publicKey?: string | null; +} + +type ChatItem = PublicChat | DMConversation; + +export function UnifiedChatsList() { + const { user, switchToPublicChat, switchToDM, chat } = useAppState(); + const { dmUsers, isLoadingUsers, loadUsers } = useDM(); + + const [publicChats] = useState([ + { id: "general", name: "Общий чат", type: "public" }, + { id: "general2", name: "Общий чат 2", type: "public" } + ]); + const [lastMessages, setLastMessages] = useState>({}); + const [allChats, setAllChats] = useState([]); + + // Load public chat last messages + const loadLastMessages = useCallback(async () => { + if (!user.authToken) return; + + try { + const response = await fetch(`${API_BASE_URL}/get_messages`, { + headers: getAuthHeaders(user.authToken) + }); + + if (response.ok) { + const data = await response.json(); + if (data.messages && data.messages.length > 0) { + const lastMessage = data.messages[data.messages.length - 1]; + + setLastMessages({ + general: lastMessage, + general2: lastMessage + }); + } + } + } catch (error) { + console.error("Error loading last messages:", error); + } + }, [user.authToken]); + + // Load DM users when chats tab is active + useEffect(() => { + if (chat.activeTab === "chats") { + loadUsers(); + loadLastMessages(); + } + }, [chat.activeTab, loadUsers, loadLastMessages]); + + // Combine public chats and DMs into one list + useEffect(() => { + const publicChatItems: ChatItem[] = publicChats.map(chat => ({ + ...chat, + lastMessage: lastMessages[chat.id] + })); + + const dmChatItems: ChatItem[] = dmUsers.map((user: DMUser) => ({ + id: user.id, + username: user.username, + profile_picture: user.profile_picture, + online: user.online, + type: "dm" as const, + lastMessage: user.lastMessage, + unreadCount: user.unreadCount, + publicKey: user.publicKey + })); + + // Combine and sort by last message timestamp (DMs first, then public chats) + const combined = [...dmChatItems, ...publicChatItems]; + setAllChats(combined); + }, [publicChats, lastMessages, dmUsers]); + + // WebSocket listener for public chat message updates + useEffect(() => { + if (!websocket) return; + + const handleWebSocketMessage = (e: MessageEvent) => { + try { + const msg = JSON.parse(e.data); + + if (msg.type === "newMessage") { + const newMessage = msg.data as Message; + // Update all public chats with the new message + setLastMessages(prev => { + const updated = { ...prev }; + publicChats.forEach(chat => { + updated[chat.id] = newMessage; + }); + return updated; + }); + } else if (msg.type === "messageEdited") { + const editedMessage = msg.data as Message; + // Update only if the edited message is the current last message + setLastMessages(prev => { + const updated = { ...prev }; + publicChats.forEach(chat => { + if (updated[chat.id]?.id === editedMessage.id) { + updated[chat.id] = editedMessage; + } + }); + return updated; + }); + } else if (msg.type === "messageDeleted") { + const deletedMessageId = msg.data?.message_id; + let needsReload = false; + + setLastMessages(prev => { + const updated = { ...prev }; + publicChats.forEach(chat => { + if (updated[chat.id]?.id === deletedMessageId) { + updated[chat.id] = undefined; + needsReload = true; + } + }); + return updated; + }); + + if (needsReload) { + loadLastMessages(); + } + } + } catch (error) { + console.error("Failed to handle WebSocket message in UnifiedChatsList:", error); + } + }; + + websocket.addEventListener("message", handleWebSocketMessage); + return () => websocket.removeEventListener("message", handleWebSocketMessage); + }, [publicChats, loadLastMessages]); + + const formatPublicChatMessage = (chatId: string): string => { + const lastMessage = lastMessages[chatId]; + if (!lastMessage) { + return ""; + } + + const isCurrentUser = lastMessage.username === user.currentUser?.username; + const prefix = isCurrentUser ? "Вы: " : `${lastMessage.username}: `; + + const maxContentLength = 50 - prefix.length; + const content = lastMessage.content.length > maxContentLength + ? lastMessage.content.substring(0, maxContentLength) + "..." + : lastMessage.content; + + return prefix + content; + }; + + + const handlePublicChatClick = async (chatName: string) => { + await switchToPublicChat(chatName); + }; + + const handleDMClick = async (dmConversation: DMConversation) => { + if (!dmConversation.publicKey) { + const authToken = useAppState.getState().user.authToken; + if (!authToken) return; + + const publicKey = await fetchUserPublicKey(dmConversation.id, authToken); + if (publicKey) { + dmConversation.publicKey = publicKey; + } else { + console.error("Failed to get public key for user:", dmConversation.id); + return; + } + } + + await switchToDM({ + userId: dmConversation.id, + username: dmConversation.username, + publicKey: dmConversation.publicKey, + profilePicture: dmConversation.profile_picture, + online: dmConversation.online || false + }); + }; + + if (isLoadingUsers) { + return ( + + ); + } + + return ( + + {allChats.map((chat) => { + if (chat.type === "public") { + return ( + handlePublicChatClick(chat.name)} + style={{ cursor: "pointer" }} + > + {formatPublicChatMessage(chat.id) && ( + + {formatPublicChatMessage(chat.id)} + + )} + {chat.name} + + ); + } else { + return ( + handleDMClick(chat)} + style={{ cursor: "pointer" }} + > + + {chat.lastMessage || "Нет сообщений"} + + {chat.username} { + (e.target as HTMLImageElement).src = defaultAvatar; + }} + /> + {chat.unreadCount > 0 && ( + + {chat.unreadCount} + + )} + + ); + } + })} + + ); +} diff --git a/frontend/src/pages/chat/ui/left/UsernameSearch.tsx b/frontend/src/pages/chat/ui/left/UsernameSearch.tsx index 0ee7f4d..af8e328 100644 --- a/frontend/src/pages/chat/ui/left/UsernameSearch.tsx +++ b/frontend/src/pages/chat/ui/left/UsernameSearch.tsx @@ -130,10 +130,12 @@ export function UsernameSearch() { handleUserClick(searchUser)} style={{ cursor: "pointer" }} > + + {searchUser.online ? "В сети" : "Не в сети"} + {searchUser.username} { if (response.type === "dmNew" && this.dmData) {