AeThex-Connect/packages/core/state/slices/messagingSlice.ts

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;