/** * Messaging State Slice * Manages conversations and messages */ import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; export interface Message { id: string; conversationId: string; senderId: string; content: string; encrypted: boolean; createdAt: string; status: 'sending' | 'sent' | 'delivered' | 'read' | 'failed'; } export interface Conversation { id: string; type: 'direct' | 'group'; participants: string[]; lastMessage?: Message; unreadCount: number; createdAt: string; updatedAt: string; } interface MessagingState { conversations: Record; messages: Record; // conversationId -> messages activeConversationId: string | null; loading: boolean; error: string | null; } const initialState: MessagingState = { conversations: {}, messages: {}, activeConversationId: null, loading: false, error: null, }; // Async thunks export const fetchConversations = createAsyncThunk( 'messaging/fetchConversations', async (_, { rejectWithValue }) => { try { const response = await fetch('/api/conversations', { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, }, }); if (!response.ok) { throw new Error('Failed to fetch conversations'); } const data = await response.json(); return data.conversations; } catch (error: any) { return rejectWithValue(error.message); } } ); export const fetchMessages = createAsyncThunk( 'messaging/fetchMessages', async (conversationId: string, { rejectWithValue }) => { try { const response = await fetch(`/api/conversations/${conversationId}/messages`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, }, }); if (!response.ok) { throw new Error('Failed to fetch messages'); } const data = await response.json(); return { conversationId, messages: data.messages }; } catch (error: any) { return rejectWithValue(error.message); } } ); export const sendMessage = createAsyncThunk( 'messaging/sendMessage', async ({ conversationId, content }: { conversationId: string; content: string }, { rejectWithValue }) => { try { const response = await fetch(`/api/conversations/${conversationId}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}`, }, body: JSON.stringify({ content }), }); if (!response.ok) { throw new Error('Failed to send message'); } const data = await response.json(); return data.message; } catch (error: any) { return rejectWithValue(error.message); } } ); // Slice const messagingSlice = createSlice({ name: 'messaging', initialState, reducers: { setActiveConversation: (state, action: PayloadAction) => { state.activeConversationId = action.payload; }, addMessage: (state, action: PayloadAction) => { const message = action.payload; const conversationId = message.conversationId; if (!state.messages[conversationId]) { state.messages[conversationId] = []; } state.messages[conversationId].push(message); // Update last message in conversation if (state.conversations[conversationId]) { state.conversations[conversationId].lastMessage = message; state.conversations[conversationId].updatedAt = message.createdAt; } }, updateMessageStatus: (state, action: PayloadAction<{ messageId: string; status: Message['status'] }>) => { const { messageId, status } = action.payload; for (const conversationId in state.messages) { const message = state.messages[conversationId].find(m => m.id === messageId); if (message) { message.status = status; break; } } }, markAsRead: (state, action: PayloadAction) => { const conversationId = action.payload; if (state.conversations[conversationId]) { state.conversations[conversationId].unreadCount = 0; } // Update message statuses if (state.messages[conversationId]) { state.messages[conversationId].forEach(message => { if (message.status === 'delivered') { message.status = 'read'; } }); } }, incrementUnread: (state, action: PayloadAction) => { const conversationId = action.payload; if (state.conversations[conversationId]) { state.conversations[conversationId].unreadCount++; } }, }, extraReducers: (builder) => { // Fetch conversations builder.addCase(fetchConversations.pending, (state) => { state.loading = true; }); builder.addCase(fetchConversations.fulfilled, (state, action) => { state.loading = false; action.payload.forEach((conv: Conversation) => { state.conversations[conv.id] = conv; }); }); builder.addCase(fetchConversations.rejected, (state, action) => { state.loading = false; state.error = action.payload as string; }); // Fetch messages builder.addCase(fetchMessages.pending, (state) => { state.loading = true; }); builder.addCase(fetchMessages.fulfilled, (state, action) => { state.loading = false; const { conversationId, messages } = action.payload; state.messages[conversationId] = messages; }); builder.addCase(fetchMessages.rejected, (state, action) => { state.loading = false; state.error = action.payload as string; }); // Send message builder.addCase(sendMessage.fulfilled, (state, action) => { const message = action.payload; const conversationId = message.conversationId; if (!state.messages[conversationId]) { state.messages[conversationId] = []; } state.messages[conversationId].push(message); // Update conversation if (state.conversations[conversationId]) { state.conversations[conversationId].lastMessage = message; state.conversations[conversationId].updatedAt = message.createdAt; } }); }, }); export const { setActiveConversation, addMessage, updateMessageStatus, markAsRead, incrementUnread, } = messagingSlice.actions; export default messagingSlice.reducer;