diff --git a/package.json b/package.json index 04eac24..01e2621 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite --host", - "build": "tsc && vite build", + "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" }, diff --git a/src/redux/slices/groupChat.ts b/src/redux/slices/groupChat.ts new file mode 100644 index 0000000..7f2645f --- /dev/null +++ b/src/redux/slices/groupChat.ts @@ -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; // 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) { + 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) => { + 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; diff --git a/src/redux/store.ts b/src/redux/store.ts index 84a6fb3..4dfa071 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -7,6 +7,7 @@ import { contestsReducer } from './slices/contests'; import { groupsReducer } from './slices/groups'; import { articlesReducer } from './slices/articles'; import { groupFeedReducer } from './slices/groupfeed'; +import { groupChatReducer } from './slices/groupChat'; // использование // import { useAppDispatch, useAppSelector } from '../redux/hooks'; @@ -27,6 +28,7 @@ export const store = configureStore({ groups: groupsReducer, articles: articlesReducer, groupfeed: groupFeedReducer, + groupchat: groupChatReducer, }, }); diff --git a/src/views/home/group/Group.tsx b/src/views/home/group/Group.tsx index db7adcc..1642452 100644 --- a/src/views/home/group/Group.tsx +++ b/src/views/home/group/Group.tsx @@ -36,7 +36,7 @@ const Group: FC = () => { } /> - } /> + } /> } /> { +interface GroupChatProps { + groupId: number; +} + +export const Chat: FC = ({ groupId }) => { const dispatch = useAppDispatch(); useEffect(() => { dispatch(setMenuActiveGroupPage('chat')); }, []); return ( -
- {' '} - Пока пусто :( +
+
+
+ {}} + placeholder="Поиск сообщений" + /> +
+ +
+
+ {/* сообщения */} +
+
+ +
footer / input bar
+
); };