mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Implement live last message update
This commit is contained in:
@@ -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<DMUser[]>([]);
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user