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:
parent
e9dd992e29
commit
bfee9acdfb
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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 />;
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue