group chat init
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
|
|||||||
190
src/redux/slices/groupChat.ts
Normal file
190
src/redux/slices/groupChat.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import axios from '../../axios';
|
||||||
|
|
||||||
|
// =========================================
|
||||||
|
// Типы
|
||||||
|
// =========================================
|
||||||
|
|
||||||
|
export type Status = 'idle' | 'loading' | 'successful' | 'failed';
|
||||||
|
|
||||||
|
export interface ChatMessage {
|
||||||
|
id: number;
|
||||||
|
groupId: number;
|
||||||
|
authorId: number;
|
||||||
|
authorUsername: string;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FetchMessagesParams {
|
||||||
|
groupId: number;
|
||||||
|
limit?: number;
|
||||||
|
afterMessageId?: number | null;
|
||||||
|
afterCreatedAt?: string | null;
|
||||||
|
timeoutSeconds?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SendMessageParams {
|
||||||
|
groupId: number;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================
|
||||||
|
// State
|
||||||
|
// =========================================
|
||||||
|
|
||||||
|
interface GroupChatState {
|
||||||
|
messages: Record<number, ChatMessage[]>; // messages[groupId][]
|
||||||
|
fetchMessages: {
|
||||||
|
status: Status;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
sendMessage: {
|
||||||
|
status: Status;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: GroupChatState = {
|
||||||
|
messages: {},
|
||||||
|
fetchMessages: {
|
||||||
|
status: 'idle',
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
sendMessage: {
|
||||||
|
status: 'idle',
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================================
|
||||||
|
// Thunks
|
||||||
|
// =========================================
|
||||||
|
|
||||||
|
// Получить сообщения группы (обычный запрос или long polling)
|
||||||
|
export const fetchGroupMessages = createAsyncThunk(
|
||||||
|
'groupChat/fetchGroupMessages',
|
||||||
|
async (params: FetchMessagesParams, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/groups/${params.groupId}/chat`, {
|
||||||
|
params: {
|
||||||
|
limit: params.limit,
|
||||||
|
afterMessageId: params.afterMessageId,
|
||||||
|
afterCreatedAt: params.afterCreatedAt,
|
||||||
|
timeoutSeconds: params.timeoutSeconds,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
groupId: params.groupId,
|
||||||
|
messages: response.data as ChatMessage[],
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
err.response?.data?.message ||
|
||||||
|
'Ошибка при получении сообщений группы',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Отправить новое сообщение
|
||||||
|
export const sendGroupMessage = createAsyncThunk(
|
||||||
|
'groupChat/sendGroupMessage',
|
||||||
|
async ({ groupId, content }: SendMessageParams, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/groups/${groupId}/chat`, {
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data as ChatMessage;
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
err.response?.data?.message || 'Ошибка при отправке сообщения',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// =========================================
|
||||||
|
// Slice
|
||||||
|
// =========================================
|
||||||
|
|
||||||
|
const groupChatSlice = createSlice({
|
||||||
|
name: 'groupChat',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
clearChat(state, action: PayloadAction<number>) {
|
||||||
|
delete state.messages[action.payload];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
// fetchGroupMessages
|
||||||
|
builder.addCase(fetchGroupMessages.pending, (state) => {
|
||||||
|
state.fetchMessages.status = 'loading';
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addCase(
|
||||||
|
fetchGroupMessages.fulfilled,
|
||||||
|
(
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
groupId: number;
|
||||||
|
messages: ChatMessage[];
|
||||||
|
}>,
|
||||||
|
) => {
|
||||||
|
state.fetchMessages.status = 'successful';
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
builder.addCase(fetchGroupMessages.rejected, (state, action: any) => {
|
||||||
|
state.fetchMessages.status = 'failed';
|
||||||
|
state.fetchMessages.error = action.payload;
|
||||||
|
});
|
||||||
|
|
||||||
|
// sendMessage
|
||||||
|
builder.addCase(sendGroupMessage.pending, (state) => {
|
||||||
|
state.sendMessage.status = 'loading';
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addCase(
|
||||||
|
sendGroupMessage.fulfilled,
|
||||||
|
(state, action: PayloadAction<ChatMessage>) => {
|
||||||
|
state.sendMessage.status = 'successful';
|
||||||
|
|
||||||
|
const msg = action.payload;
|
||||||
|
|
||||||
|
if (!state.messages[msg.groupId]) {
|
||||||
|
state.messages[msg.groupId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
state.messages[msg.groupId].push(msg);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
builder.addCase(sendGroupMessage.rejected, (state, action: any) => {
|
||||||
|
state.sendMessage.status = 'failed';
|
||||||
|
state.sendMessage.error = action.payload;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { clearChat } = groupChatSlice.actions;
|
||||||
|
export const groupChatReducer = groupChatSlice.reducer;
|
||||||
@@ -7,6 +7,7 @@ import { contestsReducer } from './slices/contests';
|
|||||||
import { groupsReducer } from './slices/groups';
|
import { groupsReducer } from './slices/groups';
|
||||||
import { articlesReducer } from './slices/articles';
|
import { articlesReducer } from './slices/articles';
|
||||||
import { groupFeedReducer } from './slices/groupfeed';
|
import { groupFeedReducer } from './slices/groupfeed';
|
||||||
|
import { groupChatReducer } from './slices/groupChat';
|
||||||
|
|
||||||
// использование
|
// использование
|
||||||
// import { useAppDispatch, useAppSelector } from '../redux/hooks';
|
// import { useAppDispatch, useAppSelector } from '../redux/hooks';
|
||||||
@@ -27,6 +28,7 @@ export const store = configureStore({
|
|||||||
groups: groupsReducer,
|
groups: groupsReducer,
|
||||||
articles: articlesReducer,
|
articles: articlesReducer,
|
||||||
groupfeed: groupFeedReducer,
|
groupfeed: groupFeedReducer,
|
||||||
|
groupchat: groupChatReducer,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const Group: FC<GroupsBlockProps> = () => {
|
|||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="home" element={<Posts groupId={groupId} />} />
|
<Route path="home" element={<Posts groupId={groupId} />} />
|
||||||
<Route path="chat" element={<Chat />} />
|
<Route path="chat" element={<Chat groupId={groupId} />} />
|
||||||
<Route path="contests" element={<Contests />} />
|
<Route path="contests" element={<Contests />} />
|
||||||
<Route
|
<Route
|
||||||
path="*"
|
path="*"
|
||||||
|
|||||||
@@ -1,17 +1,37 @@
|
|||||||
import { useEffect } from 'react';
|
import { FC, useEffect } from 'react';
|
||||||
import { useAppDispatch } from '../../../../redux/hooks';
|
import { useAppDispatch } from '../../../../redux/hooks';
|
||||||
import { setMenuActiveGroupPage } from '../../../../redux/slices/store';
|
import { setMenuActiveGroupPage } from '../../../../redux/slices/store';
|
||||||
|
import { SearchInput } from '../../../../components/input/SearchInput';
|
||||||
|
|
||||||
export const Chat = () => {
|
interface GroupChatProps {
|
||||||
|
groupId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Chat: FC<GroupChatProps> = ({ groupId }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(setMenuActiveGroupPage('chat'));
|
dispatch(setMenuActiveGroupPage('chat'));
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-y-scroll thin-dark-scrollbar flex items-center justify-center font-bold text-liquid-white text-[50px]">
|
<div className="h-full relative">
|
||||||
{' '}
|
<div className="grid grid-rows-[40px,1fr,50px] 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">
|
||||||
|
<div className="flex flex-col gap-[20px] min-h-0 h-0">
|
||||||
|
{/* сообщения */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className=" bg-red-300">footer / input bar</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user