222 lines
7.4 KiB
TypeScript
222 lines
7.4 KiB
TypeScript
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>
|
||
);
|
||
};
|