Refactor ChatPopup to filter real-time messages by sessionId, enhance message deduplication logic, and improve logging for message statistics. Update MessageFeed to handle initial scroll behavior and add keydown event for message input handling. Adjust useChatHistory to prevent automatic refetching of chat history.
This commit is contained in:
@@ -54,14 +54,39 @@ export default function ChatPopup({ sessionId: sessionIdProp }: ChatPopupProps =
|
||||
realtimeMessages: realtimeMessages.length,
|
||||
});
|
||||
|
||||
// Фильтруем realtime сообщения только для текущей сессии
|
||||
// Это важно, т.к. WebRTC сервис глобальный и может содержать сообщения из других сессий
|
||||
const sessionRealtimeMessages = realtimeMessages.filter((m) => {
|
||||
// Если у сообщения нет sessionId, включаем его (для обратной совместимости)
|
||||
// Если sessionId есть, проверяем что он совпадает с текущей сессией
|
||||
return !m.sessionId || m.sessionId === sessionId;
|
||||
});
|
||||
|
||||
// Объединяем историю и realtime сообщения
|
||||
const historyIds = new Set(historyMessages.map((m) => m.id));
|
||||
const newRealtimeMessages = realtimeMessages.filter(
|
||||
const newRealtimeMessages = sessionRealtimeMessages.filter(
|
||||
(m) => !historyIds.has(m.id)
|
||||
);
|
||||
const allMessages = [...historyMessages, ...newRealtimeMessages];
|
||||
|
||||
// Дедупликация на всякий случай - убираем дубликаты по ID
|
||||
const allMessagesMap = new Map<string, typeof historyMessages[0]>();
|
||||
[...historyMessages, ...newRealtimeMessages].forEach((msg) => {
|
||||
allMessagesMap.set(msg.id, msg);
|
||||
});
|
||||
const allMessages = Array.from(allMessagesMap.values()).sort(
|
||||
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
);
|
||||
|
||||
console.log("[ChatPopup] All messages count:", allMessages.length);
|
||||
console.log("[ChatPopup] Messages stats:", {
|
||||
sessionId,
|
||||
history: historyMessages.length,
|
||||
realtimeTotal: realtimeMessages.length,
|
||||
realtimeForSession: sessionRealtimeMessages.length,
|
||||
newRealtime: newRealtimeMessages.length,
|
||||
total: allMessages.length,
|
||||
historyIds: Array.from(historyIds),
|
||||
realtimeIds: realtimeMessages.map(m => ({ id: m.id, sessionId: m.sessionId })),
|
||||
});
|
||||
|
||||
function onMessageSend(message: string) {
|
||||
// Передаем имя пользователя и флаг авторизации
|
||||
@@ -109,6 +134,7 @@ interface MessageFeedProps {
|
||||
function MessageFeed({ messages, currentUserId, isLoading }: MessageFeedProps) {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const prevMessageCountRef = useRef(0);
|
||||
const isInitialMount = useRef(true);
|
||||
|
||||
console.log("[MessageFeed] Rendering with currentUserId:", currentUserId);
|
||||
console.log("[MessageFeed] Messages:", messages.map(m => ({
|
||||
@@ -118,6 +144,15 @@ function MessageFeed({ messages, currentUserId, isLoading }: MessageFeedProps) {
|
||||
content: m.content.substring(0, 20)
|
||||
})));
|
||||
|
||||
// Скролл при первой загрузке сообщений
|
||||
useEffect(() => {
|
||||
if (isInitialMount.current && messages.length > 0 && !isLoading) {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "instant" });
|
||||
isInitialMount.current = false;
|
||||
prevMessageCountRef.current = messages.length;
|
||||
}
|
||||
}, [messages.length, isLoading]);
|
||||
|
||||
// Умный скролл - только при добавлении новых сообщений
|
||||
useEffect(() => {
|
||||
const currentCount = messages.length;
|
||||
@@ -125,7 +160,8 @@ function MessageFeed({ messages, currentUserId, isLoading }: MessageFeedProps) {
|
||||
// Скроллим только если добавилось новое сообщение
|
||||
if (
|
||||
currentCount > prevMessageCountRef.current &&
|
||||
prevMessageCountRef.current > 0
|
||||
prevMessageCountRef.current > 0 &&
|
||||
!isInitialMount.current
|
||||
) {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
@@ -270,6 +306,34 @@ function MessageInput({
|
||||
onMessageSend(message);
|
||||
}
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
|
||||
if (e.key === "Enter") {
|
||||
if (e.ctrlKey || e.shiftKey) {
|
||||
// Ctrl+Enter или Shift+Enter - добавляем перенос строки
|
||||
e.preventDefault();
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const newMessage = message.substring(0, start) + "\n" + message.substring(end);
|
||||
|
||||
setMessage(newMessage);
|
||||
|
||||
// Устанавливаем курсор после вставленного переноса строки
|
||||
setTimeout(() => {
|
||||
textarea.selectionStart = textarea.selectionEnd = start + 1;
|
||||
}, 0);
|
||||
} else {
|
||||
// Enter без модификаторов - отправка сообщения
|
||||
e.preventDefault();
|
||||
if (message.trim().length > 0) {
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => textareaRef.current?.focus()}
|
||||
@@ -279,6 +343,7 @@ function MessageInput({
|
||||
ref={textareaRef}
|
||||
value={message}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="w-[80%] resize-none focus:outline-none my-auto text-s"
|
||||
rows={1}
|
||||
placeholder="Сообщение..."
|
||||
|
||||
@@ -43,10 +43,11 @@ export const useChatHistory = (sessionId: string | undefined, enabled = true) =>
|
||||
}
|
||||
},
|
||||
enabled: enabled && !!sessionId,
|
||||
staleTime: 1000 * 60 * 5, // 5 минут - история считается актуальной
|
||||
staleTime: Infinity, // История загружается один раз и больше не обновляется (новые сообщения приходят через WebSocket)
|
||||
gcTime: 1000 * 60 * 30, // 30 минут в кэше
|
||||
refetchOnWindowFocus: false, // Не перезагружать при фокусе
|
||||
refetchOnReconnect: false, // Не перезагружать при реконнекте
|
||||
refetchInterval: false, // Не перезагружать автоматически
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getOrCreateGuestId } from "./guestId";
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
sessionId?: string; // ID сессии, к которой относится сообщение
|
||||
senderId: string;
|
||||
senderName?: string;
|
||||
content: string;
|
||||
|
||||
@@ -114,8 +114,14 @@ function SessionPage() {
|
||||
// }
|
||||
// }, [session?.status, navigate]);
|
||||
|
||||
const { localStream, toggleAudio, isAudioMuted, toggleVideo, isVideoMuted } =
|
||||
useWebRTC(session?.id, true);
|
||||
const {
|
||||
localStream,
|
||||
toggleAudio,
|
||||
isAudioMuted,
|
||||
toggleVideo,
|
||||
isVideoMuted,
|
||||
participants,
|
||||
} = useWebRTC(session?.id, true);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -219,12 +225,17 @@ function SessionPage() {
|
||||
</div>
|
||||
</FloatingActionButton>
|
||||
<FloatingActionButton
|
||||
className="max-2xl:hidden"
|
||||
className="relative max-2xl:hidden"
|
||||
onClick={handleParticipantsOpen}
|
||||
>
|
||||
<div className="size-[1.111vw] text-white">
|
||||
<UsersFilledIcon />
|
||||
</div>
|
||||
{participants.length > 0 && (
|
||||
<div className="bg-white absolute 2xl:-top-[0.278vw] 2xl:-right-[0.278vw] -top-1 -right-1 2xl:size-[1.111vw] size-4 text-[#7B60F3] font-semibold font-mono text-caption-xs rounded-full flex items-center justify-center 2xl:pt-[0.069vw] pt-px">
|
||||
<span className="scale-[0.8]">{participants.length + 1}</span>
|
||||
</div>
|
||||
)}
|
||||
</FloatingActionButton>
|
||||
<FloatingActionButton
|
||||
className="max-2xl:hidden"
|
||||
|
||||
@@ -328,6 +328,7 @@ io.on("connection", (socket) => {
|
||||
// senderId - это либо userId (приоритет), либо guestId
|
||||
const messageToSend = {
|
||||
id: savedMessage.id,
|
||||
sessionId: savedMessage.sessionId, // Добавляем sessionId для фильтрации на клиенте
|
||||
senderId: savedMessage.userId || savedMessage.guestId,
|
||||
senderName: savedMessage.senderName,
|
||||
content: savedMessage.content,
|
||||
|
||||
Reference in New Issue
Block a user