feat(07-01): implement core reducers with message deduplication logic
GREEN phase of TDD for Task 2: - Implement addMessageFromWebSocket with full logic: * getChannelKey helper for session vs global channel keys * Channel initialization on first message * Message deduplication by msg_id using Array.some() * Sort messages by createdAt after insertion * Unread increment: only if window closed OR viewing different channel - Implement setActiveChannel: sets activeChannel + channelType - Implement openChatWindow: sets isWindowOpen, resets unread count, updates lastReadAt - Implement closeChatWindow: sets isWindowOpen false only - Export all 4 action creators All 28 tests pass. Message deduplication validated. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
60a559e58f
commit
15a658a5dc
|
|
@ -31,13 +31,95 @@ const initialState = {
|
|||
windowPosition: null
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get channel key from message
|
||||
* Session messages use 'session-{sessionId}', global uses 'global'
|
||||
* @param {Object} message - Message object
|
||||
* @returns {string} Channel key
|
||||
*/
|
||||
const getChannelKey = (message) => {
|
||||
if (message.channel === 'session' && message.sessionId) {
|
||||
return message.sessionId;
|
||||
}
|
||||
if (message.channel === 'lesson' && message.lessonSessionId) {
|
||||
return message.lessonSessionId;
|
||||
}
|
||||
return message.channel; // 'global'
|
||||
};
|
||||
|
||||
const sessionChatSlice = createSlice({
|
||||
name: 'sessionChat',
|
||||
initialState,
|
||||
reducers: {
|
||||
// Reducers will be added in Tasks 2-3
|
||||
/**
|
||||
* Add message from WebSocket
|
||||
* Handles deduplication, sorting, and unread count increment
|
||||
*/
|
||||
addMessageFromWebSocket: (state, action) => {
|
||||
const message = action.payload;
|
||||
const channel = getChannelKey(message);
|
||||
|
||||
// Initialize channel if not exists
|
||||
if (!state.messagesByChannel[channel]) {
|
||||
state.messagesByChannel[channel] = [];
|
||||
}
|
||||
|
||||
// Deduplicate by msg_id
|
||||
const exists = state.messagesByChannel[channel].some(m => m.id === message.id);
|
||||
if (exists) return;
|
||||
|
||||
// Add message
|
||||
state.messagesByChannel[channel].push(message);
|
||||
|
||||
// Sort by createdAt ASC
|
||||
state.messagesByChannel[channel].sort((a, b) =>
|
||||
new Date(a.createdAt) - new Date(b.createdAt)
|
||||
);
|
||||
|
||||
// Increment unread count if window closed OR viewing different channel
|
||||
if (!state.isWindowOpen || state.activeChannel !== channel) {
|
||||
state.unreadCounts[channel] = (state.unreadCounts[channel] || 0) + 1;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set active channel for viewing
|
||||
*/
|
||||
setActiveChannel: (state, action) => {
|
||||
const { channel, channelType } = action.payload;
|
||||
state.activeChannel = channel;
|
||||
state.channelType = channelType;
|
||||
},
|
||||
|
||||
/**
|
||||
* Open chat window
|
||||
* Resets unread count for active channel
|
||||
*/
|
||||
openChatWindow: (state) => {
|
||||
state.isWindowOpen = true;
|
||||
|
||||
// Reset unread count for active channel
|
||||
if (state.activeChannel) {
|
||||
state.unreadCounts[state.activeChannel] = 0;
|
||||
state.lastReadAt[state.activeChannel] = new Date().toISOString();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Close chat window
|
||||
* Does not affect unread counts
|
||||
*/
|
||||
closeChatWindow: (state) => {
|
||||
state.isWindowOpen = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const {} = sessionChatSlice.actions;
|
||||
export const {
|
||||
addMessageFromWebSocket,
|
||||
setActiveChannel,
|
||||
openChatWindow,
|
||||
closeChatWindow
|
||||
} = sessionChatSlice.actions;
|
||||
|
||||
export default sessionChatSlice.reducer;
|
||||
|
|
|
|||
Loading…
Reference in New Issue