This commit is contained in:
2025-10-31 16:21:18 +05:00
5 changed files with 87 additions and 8 deletions
+69 -4
View File
@@ -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="Сообщение..."
+2 -1
View File
@@ -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, // Не перезагружать автоматически
});
};
+1
View File
@@ -3,6 +3,7 @@ import { getOrCreateGuestId } from "./guestId";
export interface ChatMessage {
id: string;
sessionId?: string; // ID сессии, к которой относится сообщение
senderId: string;
senderName?: string;
content: string;
+14 -3
View File
@@ -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 (
@@ -218,12 +224,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"
+1
View File
@@ -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,