Merge branch 'main' of http://192.168.1.163:3000/inmake/stream.graff.tech-new
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 (
|
||||
@@ -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"
|
||||
|
||||
@@ -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