bad commit

This commit is contained in:
Виталий Лавшонок
2025-11-21 22:49:14 +03:00
parent 304c734169
commit abb7301c16
2 changed files with 112 additions and 37 deletions

View File

@@ -20,8 +20,6 @@ interface FetchMessagesParams {
groupId: number; groupId: number;
limit?: number; limit?: number;
afterMessageId?: number | null; afterMessageId?: number | null;
afterCreatedAt?: string | null;
timeoutSeconds?: number;
} }
interface SendMessageParams { interface SendMessageParams {
@@ -34,7 +32,10 @@ interface SendMessageParams {
// ========================================= // =========================================
interface GroupChatState { interface GroupChatState {
messages: Record<number, ChatMessage[]>; // messages[groupId][] messages: Record<number, ChatMessage[]>; // по группам
hasMore: Record<number, boolean>; // есть ли ещё старые сообщения
isInitialLoaded: Record<number, boolean>; // загружена ли первая порция
fetchMessages: { fetchMessages: {
status: Status; status: Status;
error?: string; error?: string;
@@ -47,6 +48,8 @@ interface GroupChatState {
const initialState: GroupChatState = { const initialState: GroupChatState = {
messages: {}, messages: {},
hasMore: {},
isInitialLoaded: {},
fetchMessages: { fetchMessages: {
status: 'idle', status: 'idle',
error: undefined, error: undefined,
@@ -61,7 +64,7 @@ const initialState: GroupChatState = {
// Thunks // Thunks
// ========================================= // =========================================
// Получить сообщения группы (обычный запрос или long polling) // Получение сообщений
export const fetchGroupMessages = createAsyncThunk( export const fetchGroupMessages = createAsyncThunk(
'groupChat/fetchGroupMessages', 'groupChat/fetchGroupMessages',
async (params: FetchMessagesParams, { rejectWithValue }) => { async (params: FetchMessagesParams, { rejectWithValue }) => {
@@ -70,14 +73,13 @@ export const fetchGroupMessages = createAsyncThunk(
params: { params: {
limit: params.limit, limit: params.limit,
afterMessageId: params.afterMessageId, afterMessageId: params.afterMessageId,
afterCreatedAt: params.afterCreatedAt,
timeoutSeconds: params.timeoutSeconds,
}, },
}); });
return { return {
groupId: params.groupId, groupId: params.groupId,
messages: response.data as ChatMessage[], messages: response.data as ChatMessage[],
afterMessageId: params.afterMessageId,
}; };
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(
@@ -88,7 +90,7 @@ export const fetchGroupMessages = createAsyncThunk(
}, },
); );
// Отправить новое сообщение // Отправка
export const sendGroupMessage = createAsyncThunk( export const sendGroupMessage = createAsyncThunk(
'groupChat/sendGroupMessage', 'groupChat/sendGroupMessage',
async ({ groupId, content }: SendMessageParams, { rejectWithValue }) => { async ({ groupId, content }: SendMessageParams, { rejectWithValue }) => {
@@ -96,7 +98,6 @@ export const sendGroupMessage = createAsyncThunk(
const response = await axios.post(`/groups/${groupId}/chat`, { const response = await axios.post(`/groups/${groupId}/chat`, {
content, content,
}); });
return response.data as ChatMessage; return response.data as ChatMessage;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(
@@ -116,10 +117,12 @@ const groupChatSlice = createSlice({
reducers: { reducers: {
clearChat(state, action: PayloadAction<number>) { clearChat(state, action: PayloadAction<number>) {
delete state.messages[action.payload]; delete state.messages[action.payload];
delete state.hasMore[action.payload];
delete state.isInitialLoaded[action.payload];
}, },
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
// fetchGroupMessages // fetch messages
builder.addCase(fetchGroupMessages.pending, (state) => { builder.addCase(fetchGroupMessages.pending, (state) => {
state.fetchMessages.status = 'loading'; state.fetchMessages.status = 'loading';
}); });
@@ -131,26 +134,31 @@ const groupChatSlice = createSlice({
action: PayloadAction<{ action: PayloadAction<{
groupId: number; groupId: number;
messages: ChatMessage[]; messages: ChatMessage[];
afterMessageId?: number | null;
}>, }>,
) => { ) => {
state.fetchMessages.status = 'successful'; const { groupId, messages, afterMessageId } = action.payload;
const existing = state.messages[groupId] || [];
const { groupId, messages } = action.payload; // первичная загрузка
if (!afterMessageId) {
if (!state.messages[groupId]) state.messages[groupId] = []; state.messages[groupId] = messages;
state.isInitialLoaded[groupId] = true;
// Если пришли последние сообщения — заменяем state.hasMore[groupId] = messages.length > 0;
// Если long polling — дополняем
if (messages.length > 0) {
const existing = state.messages[groupId];
// Фильтруем дубликаты
const ids = new Set(existing.map((m) => m.id));
const newMessages = messages.filter((m) => !ids.has(m.id));
state.messages[groupId] = [...existing, ...newMessages];
} }
// догружаем старые (scroll up)
else if (afterMessageId) {
const ids = new Set(existing.map((m) => m.id));
const filtered = messages.filter((m) => !ids.has(m.id));
console.log('messages', messages);
console.log('filtered', filtered);
console.log('ids', ids);
console.log('existing', existing);
state.messages[groupId] = [...existing, ...filtered];
state.hasMore[groupId] = filtered.length > 0;
}
state.fetchMessages.status = 'successful';
}, },
); );
@@ -159,7 +167,7 @@ const groupChatSlice = createSlice({
state.fetchMessages.error = action.payload; state.fetchMessages.error = action.payload;
}); });
// sendMessage // send message
builder.addCase(sendGroupMessage.pending, (state) => { builder.addCase(sendGroupMessage.pending, (state) => {
state.sendMessage.status = 'loading'; state.sendMessage.status = 'loading';
}); });
@@ -167,15 +175,11 @@ const groupChatSlice = createSlice({
builder.addCase( builder.addCase(
sendGroupMessage.fulfilled, sendGroupMessage.fulfilled,
(state, action: PayloadAction<ChatMessage>) => { (state, action: PayloadAction<ChatMessage>) => {
state.sendMessage.status = 'successful';
const msg = action.payload; const msg = action.payload;
if (!state.messages[msg.groupId])
if (!state.messages[msg.groupId]) {
state.messages[msg.groupId] = []; state.messages[msg.groupId] = [];
}
state.messages[msg.groupId].push(msg); state.messages[msg.groupId].push(msg);
state.sendMessage.status = 'successful';
}, },
); );

View File

@@ -1,18 +1,72 @@
import { FC, useEffect } from 'react'; import { FC, useEffect, useRef } from 'react';
import { useAppDispatch } from '../../../../redux/hooks'; import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
import { setMenuActiveGroupPage } from '../../../../redux/slices/store'; import { setMenuActiveGroupPage } from '../../../../redux/slices/store';
import { fetchGroupMessages } from '../../../../redux/slices/groupChat';
import { SearchInput } from '../../../../components/input/SearchInput'; import { SearchInput } from '../../../../components/input/SearchInput';
interface GroupChatProps { interface GroupChatProps {
groupId: number; groupId: number;
} }
const CHUNK_SIZE = 10;
export const Chat: FC<GroupChatProps> = ({ groupId }) => { export const Chat: FC<GroupChatProps> = ({ groupId }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const messages = useAppSelector((s) => s.groupchat.messages[groupId] || []);
const hasMore = useAppSelector((s) => s.groupchat.hasMore[groupId]);
const isInitialLoaded = useAppSelector(
(s) => s.groupchat.isInitialLoaded[groupId],
);
const scrollRef = useRef<HTMLDivElement>(null);
// активируем таб
useEffect(() => { useEffect(() => {
dispatch(setMenuActiveGroupPage('chat')); dispatch(setMenuActiveGroupPage('chat'));
}, []); }, []);
useEffect(() => {
console.log(messages);
}, [messages]);
// первичная загрузка
useEffect(() => {
dispatch(
fetchGroupMessages({
groupId,
limit: CHUNK_SIZE,
}),
);
}, [groupId]);
// автоскролл вниз после начальной загрузки
useEffect(() => {
if (!isInitialLoaded || !scrollRef.current) return;
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}, [isInitialLoaded, messages.length]);
// догрузка старых сообщений при скролле вверх
const handleScroll = () => {
const div = scrollRef.current;
if (!div || !hasMore) return;
if (div.scrollTop < 100) {
const first = messages[0];
if (!first) return;
const beforeId = first.id - CHUNK_SIZE;
console.log(beforeId);
dispatch(
fetchGroupMessages({
groupId,
limit: CHUNK_SIZE,
afterMessageId: beforeId,
}),
);
}
};
return ( return (
<div className="h-full relative"> <div className="h-full relative">
<div className="grid grid-rows-[40px,1fr,50px] h-full relative min-h-0 gap-[20px]"> <div className="grid grid-rows-[40px,1fr,50px] h-full relative min-h-0 gap-[20px]">
@@ -24,13 +78,30 @@ export const Chat: FC<GroupChatProps> = ({ groupId }) => {
/> />
</div> </div>
<div className="min-h-0 overflow-y-scroll thin-dark-scrollbar"> <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"> <div className="flex flex-col gap-[20px] min-h-0 h-0">
{/* сообщения */} {messages.map((msg) => (
<div
key={msg.id}
className="bg-gray-800 text-white p-3 rounded-lg"
>
<div className="text-sm opacity-60">
{msg.authorUsername} {msg.id}
</div>
<div>{msg.content}</div>
<div className="text-[10px] opacity-40 mt-1">
{new Date(msg.createdAt).toLocaleString()}
</div>
</div>
))}
</div> </div>
</div> </div>
<div className=" bg-red-300">footer / input bar</div> <div className="bg-red-300">footer / input bar</div>
</div> </div>
</div> </div>
); );