diff --git a/jam-ui/src/components/client/JKSessionScreen.js b/jam-ui/src/components/client/JKSessionScreen.js index 41092b16e..efda1e0c0 100644 --- a/jam-ui/src/components/client/JKSessionScreen.js +++ b/jam-ui/src/components/client/JKSessionScreen.js @@ -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(() => { diff --git a/jam-ui/src/components/client/chat/JKChatMessageList.js b/jam-ui/src/components/client/chat/JKChatMessageList.js index 881960cda..98ce9a1c1 100644 --- a/jam-ui/src/components/client/chat/JKChatMessageList.js +++ b/jam-ui/src/components/client/chat/JKChatMessageList.js @@ -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 ; diff --git a/jam-ui/src/store/features/sessionChatSlice.js b/jam-ui/src/store/features/sessionChatSlice.js index 11231fac0..6e9e2a40c 100644 --- a/jam-ui/src/store/features/sessionChatSlice.js +++ b/jam-ui/src/store/features/sessionChatSlice.js @@ -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) => {