Files
LiquidCode_Frontend/src/views/home/group/chat/Chat.tsx
Виталий Лавшонок 390f1f52c8 add group chat
2025-11-23 10:30:31 +03:00

222 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { FC, useEffect, useRef, useState } from 'react';
import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
import { setMenuActiveGroupPage } from '../../../../redux/slices/store';
import {
fetchGroupMessages,
sendGroupMessage,
setGroupChatStatus,
} from '../../../../redux/slices/groupChat';
import { SearchInput } from '../../../../components/input/SearchInput';
import { MessageItem } from './MessageItem';
import { Send } from '../../../../assets/icons/input';
interface GroupChatProps {
groupId: number;
}
const CHUNK_SIZE = 10;
export const Chat: FC<GroupChatProps> = ({ groupId }) => {
const dispatch = useAppDispatch();
const messages = useAppSelector((s) => s.groupchat.messages[groupId] || []);
const messagesState = useAppSelector(
(state) => state.groupchat.fetchMessages.status,
);
const lastMessageId = useAppSelector(
(state) => state.groupchat.lastMessage[groupId] || 0,
);
const user = useAppSelector((state) => state.auth);
const [text, setText] = useState<string>('');
const [firstMessagesFetch, setFirctMessagesFetch] = useState<boolean>(true);
const scrollRef = useRef<HTMLDivElement>(null);
// добавлено: ref для хранения предыдущей высоты
const prevHeightRef = useRef(0);
// активируем таб
useEffect(() => {
dispatch(setMenuActiveGroupPage('chat'));
}, []);
// первичная загрузка
useEffect(() => {
dispatch(
fetchGroupMessages({
groupId,
limit: CHUNK_SIZE,
}),
);
}, [groupId]);
// автоскролл вниз после начальной загрузки (но не при догрузке)
useEffect(() => {
const div = scrollRef.current;
if (!div) return;
// если prevHeightRef == 0 — значит это не догрузка, а обычная загрузка
if (prevHeightRef.current === 0) {
div.scrollTop = div.scrollHeight;
}
}, [messages.length]);
// добавлено: компенсирование скролла при догрузке
useEffect(() => {
const div = scrollRef.current;
if (!div) return;
if (prevHeightRef.current > 0) {
const diff = div.scrollHeight - prevHeightRef.current;
div.scrollTop = diff; // компенсируем смещение
prevHeightRef.current = 0; // сбрасываем
}
}, [messages]);
useEffect(() => {
if (messagesState == 'successful') {
dispatch(
setGroupChatStatus({ key: 'fetchMessages', status: 'idle' }),
);
}
if (messagesState == 'failed') {
dispatch(
setGroupChatStatus({ key: 'fetchMessages', status: 'idle' }),
);
}
}, [messagesState]);
const lastMessageIdRef = useRef<number | null>(null);
useEffect(() => {
lastMessageIdRef.current = lastMessageId;
if (firstMessagesFetch) {
setFirctMessagesFetch(false);
dispatch(
fetchGroupMessages({
groupId,
afterMessageId: lastMessageIdRef.current,
timeoutSeconds: 10,
}),
);
}
}, [messages]);
useEffect(() => {
const interval = setInterval(() => {
if (lastMessageIdRef.current === null) return;
dispatch(
fetchGroupMessages({
groupId,
afterMessageId: lastMessageIdRef.current,
timeoutSeconds: 10,
}),
);
}, 10000);
return () => clearInterval(interval);
}, [groupId]);
const handleSend = () => {
if (!text.trim()) return;
dispatch(
sendGroupMessage({
groupId,
content: text.trim(),
}),
).then(() => {
setText('');
setTimeout(() => {
const div = scrollRef.current;
if (div) div.scrollTop = div.scrollHeight;
}, 0);
});
};
const handleEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSend();
}
};
// догрузка старых сообщений при скролле вверх
const handleScroll = () => {
const div = scrollRef.current;
if (!div) return;
// если скролл в верхней точке
if (div.scrollTop === 0) {
prevHeightRef.current = div.scrollHeight; // запоминаем высоту до загрузки
const first = messages[0];
if (!first || first.id == 1) return;
const beforeId = first.id - CHUNK_SIZE;
dispatch(
fetchGroupMessages({
groupId,
limit: CHUNK_SIZE,
afterMessageId: beforeId,
}),
);
}
};
return (
<div className="h-full relative">
<div className="grid grid-rows-[40px,1fr,40px] h-full relative min-h-0 gap-[20px]">
<div className="relative">
<SearchInput
className="w-[216px]"
onChange={() => {}}
placeholder="Поиск сообщений"
/>
</div>
<div
className="min-h-0 overflow-y-scroll thin-dark-scrollbar"
ref={scrollRef}
onScroll={handleScroll}
>
<div className="flex flex-col gap-[20px] min-h-0 h-0 px-[16px]">
{messages.map((msg, i) => (
<MessageItem
key={i}
message={msg.content}
createdAt={msg.createdAt}
id={msg.id}
groupId={msg.groupId}
authorId={msg.authorId}
authorUsername={msg.authorUsername}
myMessage={msg.authorId == Number(user.id)}
/>
))}
</div>
</div>
<label className="bg-liquid-lighter rounded-[10px] cursor-text flex items-center px-[16px]">
<input
className="w-[calc(100%-50px)] outline-none bg-transparent placeholder:text-[16px] placeholder:text-liquid-light placeholder:font-medium"
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={handleEnter}
placeholder="Введите сообщение"
/>
<img
src={Send}
className=" absolute cursor-pointer right-[16px] active:scale-90 transition-all duration-300"
onClick={() => {
handleSend();
}}
/>
</label>
</div>
</div>
);
};