bad commit
This commit is contained in:
@@ -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';
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user