229 lines
6.5 KiB
TypeScript
229 lines
6.5 KiB
TypeScript
/**
|
|
* 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<string, Conversation>;
|
|
messages: Record<string, Message[]>; // 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<string>) => {
|
|
state.activeConversationId = action.payload;
|
|
},
|
|
addMessage: (state, action: PayloadAction<Message>) => {
|
|
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<string>) => {
|
|
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<string>) => {
|
|
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;
|