310 lines
10 KiB
JavaScript
310 lines
10 KiB
JavaScript
import { useEffect } from 'react';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import { useJamServerContext } from '../context/JamServerContext';
|
|
import {
|
|
addParticipant,
|
|
removeParticipant,
|
|
updateParticipant,
|
|
addUserTrack,
|
|
removeUserTrack,
|
|
updateUserTrack,
|
|
addJamTrack,
|
|
removeJamTrack,
|
|
addBackingTrack,
|
|
removeBackingTrack,
|
|
startRecording,
|
|
stopRecording,
|
|
addRecordedTrack,
|
|
setConnectionStatus,
|
|
setAvailableMixdowns
|
|
} from '../store/features/activeSessionSlice';
|
|
import {
|
|
updateMediaSummary,
|
|
setMetronome
|
|
} from '../store/features/mixersSlice';
|
|
import {
|
|
setBackingTracks,
|
|
setJamTracks,
|
|
setRecordedTracks,
|
|
updateJamTrackState,
|
|
setDownloadState
|
|
} from '../store/features/mediaSlice';
|
|
import {
|
|
addMessageFromWebSocket,
|
|
incrementUnreadCount
|
|
} from '../store/features/sessionChatSlice';
|
|
|
|
/**
|
|
* Helper function to get channel key from WebSocket chat message
|
|
*
|
|
* Channel keys map Protocol Buffer channel types to Redux state keys:
|
|
* - Session messages: Use session_id directly
|
|
* - Lesson messages: Use lesson_session_id directly
|
|
* - Global messages: Use 'global' literal
|
|
*
|
|
* @param {Object} message - WebSocket message with channel and ID fields
|
|
* @param {string} message.channel - Channel type ('session', 'lesson', or 'global')
|
|
* @param {string} [message.session_id] - Session ID (for session channel)
|
|
* @param {string} [message.lesson_session_id] - Lesson session ID (for lesson channel)
|
|
* @returns {string} Channel key for Redux state
|
|
*/
|
|
const getChannelKeyFromMessage = (message) => {
|
|
if (message.channel === 'session' && message.session_id) {
|
|
return message.session_id;
|
|
}
|
|
if (message.channel === 'lesson' && message.lesson_session_id) {
|
|
return message.lesson_session_id;
|
|
}
|
|
return 'global';
|
|
};
|
|
|
|
/**
|
|
* Custom hook to integrate WebSocket messages with Redux state
|
|
* Listens to WebSocket events and dispatches appropriate Redux actions
|
|
*
|
|
* @param {string} sessionId - The active session ID
|
|
*/
|
|
export const useSessionWebSocket = (sessionId) => {
|
|
const dispatch = useDispatch();
|
|
const { jamServer, isConnected } = useJamServerContext();
|
|
|
|
// Get chat state for unread increment logic
|
|
const chatState = useSelector((state) => state.sessionChat || { isWindowOpen: false, activeChannel: null });
|
|
|
|
useEffect(() => {
|
|
if (!jamServer || !sessionId) return;
|
|
|
|
// Update connection status
|
|
dispatch(setConnectionStatus(isConnected ? 'connected' : 'disconnected'));
|
|
|
|
// Define message callbacks that dispatch Redux actions
|
|
const callbacks = {
|
|
// Participant events
|
|
participantJoined: (data) => {
|
|
console.log('Participant joined:', data);
|
|
dispatch(addParticipant(data.participant));
|
|
},
|
|
|
|
participantLeft: (data) => {
|
|
console.log('Participant left:', data);
|
|
dispatch(removeParticipant(data.participantId));
|
|
},
|
|
|
|
participantUpdated: (data) => {
|
|
console.log('Participant updated:', data);
|
|
dispatch(updateParticipant(data.participant));
|
|
},
|
|
|
|
// Track events
|
|
trackAdded: (data) => {
|
|
console.log('Track added:', data);
|
|
dispatch(addUserTrack(data.track));
|
|
},
|
|
|
|
trackRemoved: (data) => {
|
|
console.log('Track removed:', data);
|
|
dispatch(removeUserTrack(data.trackId));
|
|
},
|
|
|
|
trackUpdated: (data) => {
|
|
console.log('Track updated:', data);
|
|
dispatch(updateUserTrack(data.track));
|
|
},
|
|
|
|
// Jam Track events
|
|
jamTrackAdded: (data) => {
|
|
console.log('Jam track added:', data);
|
|
dispatch(addJamTrack(data.jamTrack));
|
|
},
|
|
|
|
jamTrackRemoved: (data) => {
|
|
console.log('Jam track removed:', data);
|
|
dispatch(removeJamTrack(data.jamTrackId));
|
|
},
|
|
|
|
// Backing Track events
|
|
backingTrackAdded: (data) => {
|
|
console.log('Backing track added:', data);
|
|
dispatch(addBackingTrack(data.backingTrack));
|
|
},
|
|
|
|
backingTrackRemoved: (data) => {
|
|
console.log('Backing track removed:', data);
|
|
dispatch(removeBackingTrack(data.backingTrackId));
|
|
},
|
|
|
|
// Recording events
|
|
recordingStarted: (data) => {
|
|
console.log('Recording started:', data);
|
|
dispatch(startRecording(data.recordingId));
|
|
},
|
|
|
|
recordingStopped: (data) => {
|
|
console.log('Recording stopped:', data);
|
|
dispatch(stopRecording());
|
|
},
|
|
|
|
recordedTrackReady: (data) => {
|
|
console.log('Recorded track ready:', data);
|
|
dispatch(addRecordedTrack(data.track));
|
|
},
|
|
|
|
// Phase 5: Mixer and Media events
|
|
// NOTE: MIXER_CHANGES message type not currently sent by server
|
|
// Media arrays (backingTracks, jamTracks, recordedTracks) are now extracted
|
|
// from session REST API response in useSessionModel.js
|
|
MIXER_CHANGES: (sessionMixers) => {
|
|
console.log('[MIXER_CHANGES] Received (legacy handler):', sessionMixers);
|
|
const session = sessionMixers.session;
|
|
const mixers = sessionMixers.mixers;
|
|
|
|
// Update media summary
|
|
if (mixers.mediaSummary) {
|
|
dispatch(updateMediaSummary(mixers.mediaSummary));
|
|
}
|
|
|
|
// Update media arrays
|
|
dispatch(setBackingTracks(mixers.backingTracks || []));
|
|
dispatch(setJamTracks(mixers.jamTracks || []));
|
|
dispatch(setRecordedTracks(mixers.recordedTracks || []));
|
|
dispatch(setMetronome(mixers.metronome || null));
|
|
|
|
// Phase 5 Plan 2: Handle JamTrack mixdown changes
|
|
if (mixers.jamTrackMixdowns) {
|
|
dispatch(setAvailableMixdowns(mixers.jamTrackMixdowns));
|
|
}
|
|
},
|
|
|
|
JAM_TRACK_CHANGES: (changes) => {
|
|
console.log('Jam track changes received:', changes);
|
|
dispatch(updateJamTrackState({
|
|
isPlaying: changes.jamTrackState?.isPlaying || false,
|
|
isPaused: changes.jamTrackState?.isPaused || false,
|
|
currentPositionMs: parseInt(changes.jamTrackState?.currentPositionMs || '0', 10),
|
|
lastUpdate: Date.now()
|
|
}));
|
|
},
|
|
|
|
MIXDOWN_CHANGES: (message) => {
|
|
console.log('Mixdown changes received:', message);
|
|
if (message.mixdownPackage) {
|
|
dispatch(setDownloadState({
|
|
currentStep: message.mixdownPackage.current_packaging_step || 0,
|
|
totalSteps: message.mixdownPackage.packaging_steps || 0
|
|
}));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Phase 7 Plan 3: Handle CHAT_MESSAGE from WebSocket
|
|
*
|
|
* Transforms Protocol Buffer message format to Redux format and handles
|
|
* unread count increment based on chat window state.
|
|
*
|
|
* Unread increment logic:
|
|
* - Increment if window is closed
|
|
* - Increment if window is open but viewing a different channel
|
|
* - Do NOT increment if window is open and viewing the same channel
|
|
*
|
|
* @param {Object} message - Protocol Buffer formatted chat message
|
|
*/
|
|
CHAT_MESSAGE: (message) => {
|
|
console.log('Chat message received:', message);
|
|
|
|
// Transform Protocol Buffer format to Redux format
|
|
const chatMessage = {
|
|
id: message.msg_id,
|
|
senderId: message.user_id,
|
|
senderName: message.user_name,
|
|
message: message.message,
|
|
channel: message.channel,
|
|
sessionId: message.session_id || null,
|
|
lessonSessionId: message.lesson_session_id || null,
|
|
createdAt: message.created_at,
|
|
purpose: message.purpose || null,
|
|
musicNotation: message.music_notation || null,
|
|
claimedRecording: message.claimed_recording || null
|
|
};
|
|
|
|
// Add message to Redux state
|
|
dispatch(addMessageFromWebSocket(chatMessage));
|
|
|
|
// Increment unread count if window closed or viewing different channel
|
|
const messageChannelKey = getChannelKeyFromMessage(message);
|
|
const shouldIncrementUnread = !chatState.isWindowOpen || chatState.activeChannel !== messageChannelKey;
|
|
if (shouldIncrementUnread) {
|
|
dispatch(incrementUnreadCount({ channel: messageChannelKey }));
|
|
}
|
|
},
|
|
|
|
// Handle WebSocket subscription notifications (e.g., mixdown packaging progress)
|
|
SUBSCRIPTION_MESSAGE: (header, payload) => {
|
|
console.log('[WebSocket] Subscription message received:', { header, payload });
|
|
|
|
// Parse the payload body (may be JSON string)
|
|
let body;
|
|
try {
|
|
body = typeof payload.body === 'string' ? JSON.parse(payload.body) : payload.body;
|
|
} catch (err) {
|
|
console.error('[WebSocket] Failed to parse subscription message body:', err);
|
|
return;
|
|
}
|
|
|
|
// Handle mixdown packaging progress
|
|
if (payload.type === 'mixdown' && body) {
|
|
dispatch(setDownloadState({
|
|
signing_state: body.signing_state,
|
|
packaging_steps: body.packaging_steps || 0,
|
|
current_packaging_step: body.current_packaging_step || 0
|
|
}));
|
|
|
|
console.log(`[WebSocket] Mixdown packaging: ${body.signing_state}, step ${body.current_packaging_step}/${body.packaging_steps}`);
|
|
|
|
// If packaging failed, set error state
|
|
if (body.signing_state === 'ERROR' ||
|
|
body.signing_state === 'SIGNING_TIMEOUT' ||
|
|
body.signing_state === 'QUEUED_TIMEOUT' ||
|
|
body.signing_state === 'QUIET_TIMEOUT') {
|
|
dispatch(setDownloadState({
|
|
state: 'error',
|
|
error: {
|
|
type: 'download',
|
|
message: `Packaging failed: ${body.signing_state}`
|
|
}
|
|
}));
|
|
}
|
|
}
|
|
},
|
|
|
|
// Connection events
|
|
connectionStatusChanged: (data) => {
|
|
console.log('Connection status changed:', data);
|
|
dispatch(setConnectionStatus(data.status));
|
|
}
|
|
};
|
|
|
|
// Register callbacks with jamServer
|
|
// Note: The actual event names will depend on your WebSocket implementation
|
|
// Adjust these based on your Protocol Buffer message types
|
|
Object.entries(callbacks).forEach(([event, callback]) => {
|
|
if (jamServer.on) {
|
|
jamServer.on(event, callback);
|
|
} else if (jamServer.registerMessageCallback) {
|
|
// If using the registerMessageCallback pattern from the legacy code
|
|
jamServer.registerMessageCallback(event, callback);
|
|
}
|
|
});
|
|
|
|
// Cleanup function to unregister callbacks
|
|
return () => {
|
|
Object.entries(callbacks).forEach(([event, callback]) => {
|
|
if (jamServer.off) {
|
|
jamServer.off(event, callback);
|
|
} else if (jamServer.unregisterMessageCallback) {
|
|
jamServer.unregisterMessageCallback(event, callback);
|
|
}
|
|
});
|
|
};
|
|
}, [jamServer, sessionId, isConnected, dispatch, chatState]);
|
|
};
|