fix(15): resolve UAT issues - uploader message visibility and chat history

Issues found during UAT:
1. Uploader doesn't see their own attachment message
2. Chat history doesn't load on page refresh/rejoin

Root causes:
1. Backend's server_publish_to_session excludes sender from WebSocket
   broadcast (exclude_client_id: sender[:client_id])
2. fetchChatHistory was imported but never called in JKChatMessageList

Fixes:
- Add optimistic message in uploadAttachment.fulfilled for uploader
  Since sender is excluded from WebSocket, we add the message locally
  using the MusicNotation response + user info
- Add useEffect in JKChatMessageList to dispatch fetchChatHistory
  when channel becomes active

Technical details:
- Pass userId/userName to uploadAttachment thunk for message construction
- Use 'attachment-{notation.id}' as message ID to avoid collision
- Fetch history when fetchStatus is not 'loading' or 'succeeded'

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-02-06 15:36:04 +05:30
parent e9dd992e29
commit bfee9acdfb
3 changed files with 71 additions and 7 deletions

View File

@ -1029,13 +1029,16 @@ const JKSessionScreen = () => {
// Open chat window if not already open
dispatch(openModal('chat'));
// Dispatch upload
// Dispatch upload with user info for optimistic update
// (sender is excluded from WebSocket broadcast, so we add message locally)
dispatch(uploadAttachment({
file,
sessionId,
clientId: server?.clientId
clientId: server?.clientId,
userId: currentUser?.id,
userName: currentUser?.name
}));
}, [dispatch, sessionId, server?.clientId]);
}, [dispatch, sessionId, server?.clientId, currentUser?.id, currentUser?.name]);
// Show error toast when upload fails
useEffect(() => {

View File

@ -125,6 +125,16 @@ const JKChatMessageList = () => {
};
}, []);
/**
* Fetch chat history when channel becomes active
* Only fetches if we haven't fetched yet for this channel
*/
useEffect(() => {
if (activeChannel && fetchStatus !== 'loading' && fetchStatus !== 'succeeded') {
dispatch(fetchChatHistory({ channel: activeChannel }));
}
}, [dispatch, activeChannel, fetchStatus]);
// Loading state
if (fetchStatus === 'loading' && messages.length === 0) {
return <JKChatLoadingSpinner />;

View File

@ -57,7 +57,7 @@ export const sendMessage = createAsyncThunk(
*/
export const uploadAttachment = createAsyncThunk(
'sessionChat/uploadAttachment',
async ({ file, sessionId, clientId }, { rejectWithValue }) => {
async ({ file, sessionId, clientId, userId, userName }, { rejectWithValue }) => {
try {
// Build FormData
const formData = new FormData();
@ -73,8 +73,15 @@ export const uploadAttachment = createAsyncThunk(
// Upload via REST API
const response = await uploadMusicNotation(formData);
// Response contains file metadata, but ChatMessage arrives via WebSocket
return response;
// Return response with user info for optimistic update
// (sender is excluded from WebSocket broadcast by backend)
return {
musicNotations: response,
sessionId,
userId,
userName,
attachmentType
};
} catch (error) {
// Check for specific error types
if (error.status === 413) {
@ -418,7 +425,51 @@ const sessionChatSlice = createSlice({
state.uploadState.status = 'idle';
state.uploadState.fileName = null;
state.uploadState.progress = 0;
// Note: Actual chat message arrives via WebSocket, not from upload response
// Add attachment message locally for the uploader
// (sender is excluded from WebSocket broadcast by backend's server_publish_to_session)
const { musicNotations, sessionId, userId, userName, attachmentType } = action.payload;
if (musicNotations && musicNotations.length > 0 && sessionId) {
const notation = musicNotations[0]; // Single file upload
const channel = sessionId; // Session channel uses sessionId as key
// Initialize channel if not exists
if (!state.messagesByChannel[channel]) {
state.messagesByChannel[channel] = [];
}
// Construct attachment message from upload response
// Use music_notation.id as part of message id to avoid collision with real msg_id
const messageId = `attachment-${notation.id}`;
// Check for duplicates (in case WebSocket somehow delivers to sender)
const exists = state.messagesByChannel[channel].some(m => m.id === messageId);
if (!exists) {
const purpose = attachmentType === 'audio' ? 'Audio File' : 'Notation File';
const message = {
id: messageId,
senderId: userId,
senderName: userName || 'You',
message: '',
createdAt: notation.created_at || new Date().toISOString(),
channel: 'session',
sessionId: sessionId,
purpose: purpose,
attachmentId: notation.id,
attachmentType: attachmentType,
attachmentName: notation.file_name,
attachmentSize: notation.size || null
};
state.messagesByChannel[channel].push(message);
// Sort by createdAt ASC
state.messagesByChannel[channel].sort((a, b) =>
new Date(a.createdAt) - new Date(b.createdAt)
);
}
}
})
// uploadAttachment rejected
.addCase(uploadAttachment.rejected, (state, action) => {