diff --git a/src/redux/slices/groupChat.ts b/src/redux/slices/groupChat.ts index 7f2645f..e97db39 100644 --- a/src/redux/slices/groupChat.ts +++ b/src/redux/slices/groupChat.ts @@ -20,8 +20,6 @@ interface FetchMessagesParams { groupId: number; limit?: number; afterMessageId?: number | null; - afterCreatedAt?: string | null; - timeoutSeconds?: number; } interface SendMessageParams { @@ -34,7 +32,10 @@ interface SendMessageParams { // ========================================= interface GroupChatState { - messages: Record; // messages[groupId][] + messages: Record; // по группам + hasMore: Record; // есть ли ещё старые сообщения + isInitialLoaded: Record; // загружена ли первая порция + fetchMessages: { status: Status; error?: string; @@ -47,6 +48,8 @@ interface GroupChatState { const initialState: GroupChatState = { messages: {}, + hasMore: {}, + isInitialLoaded: {}, fetchMessages: { status: 'idle', error: undefined, @@ -61,7 +64,7 @@ const initialState: GroupChatState = { // Thunks // ========================================= -// Получить сообщения группы (обычный запрос или long polling) +// Получение сообщений export const fetchGroupMessages = createAsyncThunk( 'groupChat/fetchGroupMessages', async (params: FetchMessagesParams, { rejectWithValue }) => { @@ -70,14 +73,13 @@ export const fetchGroupMessages = createAsyncThunk( params: { limit: params.limit, afterMessageId: params.afterMessageId, - afterCreatedAt: params.afterCreatedAt, - timeoutSeconds: params.timeoutSeconds, }, }); return { groupId: params.groupId, messages: response.data as ChatMessage[], + afterMessageId: params.afterMessageId, }; } catch (err: any) { return rejectWithValue( @@ -88,7 +90,7 @@ export const fetchGroupMessages = createAsyncThunk( }, ); -// Отправить новое сообщение +// Отправка export const sendGroupMessage = createAsyncThunk( 'groupChat/sendGroupMessage', async ({ groupId, content }: SendMessageParams, { rejectWithValue }) => { @@ -96,7 +98,6 @@ export const sendGroupMessage = createAsyncThunk( const response = await axios.post(`/groups/${groupId}/chat`, { content, }); - return response.data as ChatMessage; } catch (err: any) { return rejectWithValue( @@ -116,10 +117,12 @@ const groupChatSlice = createSlice({ reducers: { clearChat(state, action: PayloadAction) { delete state.messages[action.payload]; + delete state.hasMore[action.payload]; + delete state.isInitialLoaded[action.payload]; }, }, extraReducers: (builder) => { - // fetchGroupMessages + // fetch messages builder.addCase(fetchGroupMessages.pending, (state) => { state.fetchMessages.status = 'loading'; }); @@ -131,26 +134,31 @@ const groupChatSlice = createSlice({ action: PayloadAction<{ groupId: number; 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 (!state.messages[groupId]) state.messages[groupId] = []; - - // Если пришли последние сообщения — заменяем - // Если 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]; + // первичная загрузка + if (!afterMessageId) { + state.messages[groupId] = messages; + state.isInitialLoaded[groupId] = true; + state.hasMore[groupId] = messages.length > 0; } + // догружаем старые (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; }); - // sendMessage + // send message builder.addCase(sendGroupMessage.pending, (state) => { state.sendMessage.status = 'loading'; }); @@ -167,15 +175,11 @@ const groupChatSlice = createSlice({ builder.addCase( sendGroupMessage.fulfilled, (state, action: PayloadAction) => { - state.sendMessage.status = 'successful'; - const msg = action.payload; - - if (!state.messages[msg.groupId]) { + if (!state.messages[msg.groupId]) state.messages[msg.groupId] = []; - } - state.messages[msg.groupId].push(msg); + state.sendMessage.status = 'successful'; }, ); diff --git a/src/views/home/group/chat/Chat.tsx b/src/views/home/group/chat/Chat.tsx index 03215d9..dc7bd71 100644 --- a/src/views/home/group/chat/Chat.tsx +++ b/src/views/home/group/chat/Chat.tsx @@ -1,18 +1,72 @@ -import { FC, useEffect } from 'react'; -import { useAppDispatch } from '../../../../redux/hooks'; +import { FC, useEffect, useRef } from 'react'; +import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; import { setMenuActiveGroupPage } from '../../../../redux/slices/store'; +import { fetchGroupMessages } from '../../../../redux/slices/groupChat'; import { SearchInput } from '../../../../components/input/SearchInput'; interface GroupChatProps { groupId: number; } +const CHUNK_SIZE = 10; + export const Chat: FC = ({ groupId }) => { 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(null); + + // активируем таб useEffect(() => { 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 (
@@ -24,13 +78,30 @@ export const Chat: FC = ({ groupId }) => { />
-
+
- {/* сообщения */} + {messages.map((msg) => ( +
+
+ {msg.authorUsername} {msg.id} +
+
{msg.content}
+
+ {new Date(msg.createdAt).toLocaleString()} +
+
+ ))}
-
footer / input bar
+
footer / input bar
);