1740 lines
68 KiB
JavaScript
1740 lines
68 KiB
JavaScript
// jam-ui/src/components/client/JKSessionScreen.js
|
||
import React, { useEffect, useRef, useState, memo, useMemo, useCallback } from 'react'
|
||
import { useParams, useHistory } from 'react-router-dom';
|
||
import { useDispatch, useSelector } from 'react-redux';
|
||
|
||
//import useJamServer, { ConnectionStatus } from '../../hooks/useJamServer'
|
||
import useGearUtils from '../../hooks/useGearUtils'
|
||
import useSessionUtils from '../../hooks/useSessionUtils.js';
|
||
import useSessionModel from '../../hooks/useSessionModel.js';
|
||
import useSessionHelper from '../../hooks/useSessionHelper.js';
|
||
import useRecordingHelpers from '../../hooks/useRecordingHelpers.js';
|
||
import useMixerStore from '../../hooks/useMixerStore.js';
|
||
|
||
import { useJamServerContext } from '../../context/JamServerContext.js';
|
||
import { useGlobalContext } from '../../context/GlobalContext.js';
|
||
import { useJamKazamApp } from '../../context/JamKazamAppContext.js';
|
||
import { useMixersContext } from '../../context/MixersContext.js';
|
||
import { useAuth } from '../../context/UserAuth';
|
||
import useMediaActions from '../../hooks/useMediaActions';
|
||
|
||
import { dkeys } from '../../helpers/utils.js';
|
||
|
||
import { getSessionHistory, getSession, joinSession as joinSessionRest, updateSessionSettings, getFriends, startRecording, stopRecording, submitSessionFeedback, getVideoConferencingRoomUrl, getJamTrack, closeJamTrack, openMetronome } from '../../helpers/rest';
|
||
import { syncTracksToServer } from '../../services/trackSyncService';
|
||
|
||
// Redux imports
|
||
import { openModal, closeModal, toggleModal, selectModal } from '../../store/features/sessionUISlice';
|
||
import { selectMediaSummary, selectMetronomeTrackMixers, selectMixersReady } from '../../store/features/mixersSlice';
|
||
import {
|
||
fetchActiveSession,
|
||
joinActiveSession,
|
||
leaveActiveSession,
|
||
setGuardsPassed,
|
||
setUserTracks,
|
||
setConnectionStatus,
|
||
setSessionId,
|
||
updateSessionData,
|
||
setSelectedJamTrack,
|
||
setJamTrackStems,
|
||
setBackingTrackData,
|
||
clearBackingTrackData,
|
||
setJamTrackData,
|
||
clearJamTrackData,
|
||
clearSession,
|
||
selectActiveSession,
|
||
selectJoinStatus,
|
||
selectHasJoined,
|
||
selectGuardsPassed,
|
||
selectUserTracks,
|
||
selectShowConnectionAlert,
|
||
selectSessionId,
|
||
selectInSession,
|
||
selectSelectedJamTrack,
|
||
selectJamTrackStems,
|
||
selectBackingTrackData,
|
||
selectJamTrackData
|
||
} from '../../store/features/activeSessionSlice';
|
||
import { addMessageFromWebSocket, uploadAttachment, selectIsUploading, selectUploadError, selectUploadFileName, selectUploadStatus, clearUploadError, fetchChatHistory } from '../../store/features/sessionChatSlice';
|
||
import { validateFile } from '../../services/attachmentValidation';
|
||
|
||
import { CLIENT_ROLE, RECORD_TYPE_AUDIO, RECORD_TYPE_BOTH } from '../../helpers/globals';
|
||
import { MessageType } from '../../helpers/MessageFactory.js';
|
||
|
||
import { Alert, Col, Row, Button, Card, CardBody, Modal, ModalHeader, ModalBody, ModalFooter, CardHeader, Badge, UncontrolledTooltip, Spinner } from 'reactstrap';
|
||
import FalconCardHeader from '../common/FalconCardHeader';
|
||
import SessionTrackVU from './SessionTrackVU.js';
|
||
import JKSessionMyTrack from './JKSessionMyTrack.js';
|
||
import JKSessionAudioInputs from './JKSessionAudioInputs.js';
|
||
import JKSessionRemoteTracks from './JKSessionRemoteTracks.js';
|
||
import JKSessionSettingsModal from './JKSessionSettingsModal.js';
|
||
import JKSessionInviteModal from './JKSessionInviteModal.js';
|
||
import JKSessionVolumeModal from './JKSessionVolumeModal.js';
|
||
import JKSessionRecordingModal from './JKSessionRecordingModal.js';
|
||
import JKSessionLeaveModal from './JKSessionLeaveModal.js';
|
||
import JKSessionJamTrackModal from './JKSessionJamTrackModal.js';
|
||
import JKSessionJamTrackStems from './JKSessionJamTrackStems.js';
|
||
import JKSessionOpenMenu from './JKSessionOpenMenu.js';
|
||
import WindowPortal from '../common/WindowPortal.js';
|
||
import JKSessionBackingTrackPlayer from './JKSessionBackingTrackPlayer.js';
|
||
import JKSessionJamTrackPlayer from './JKSessionJamTrackPlayer.js';
|
||
import JKSessionBackingTrack from './JKSessionBackingTrack.js';
|
||
import JKSessionMetronome from './JKSessionMetronome.js';
|
||
import JKSessionMetronomePlayer from './JKSessionMetronomePlayer.js';
|
||
import JKSessionChatWindow from './JKSessionChatWindow.js';
|
||
import JKSessionChatButton from './JKSessionChatButton.js';
|
||
import JKPopupMediaControls from '../popups/JKPopupMediaControls.js';
|
||
import { SESSION_PRIVACY_MAP } from '../../helpers/globals.js';
|
||
import { toast } from 'react-toastify';
|
||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||
|
||
//icon imports
|
||
import gearIcon from '../../assets/img/client/gear.svg';
|
||
import inviteIcon from '../../assets/img/client/invite.svg';
|
||
import volumeIcon from '../../assets/img/client/volume.svg';
|
||
import videoIcon from '../../assets/img/client/video.svg';
|
||
import recordIcon from '../../assets/img/client/record.svg';
|
||
import broadcastIcon from '../../assets/img/client/broadcast.svg';
|
||
import openIcon from '../../assets/img/client/open.svg';
|
||
import chatIcon from '../../assets/img/client/chat.svg';
|
||
import attachIcon from '../../assets/img/client/attach.svg';
|
||
import resyncIcon from '../../assets/img/client/resync.svg';
|
||
|
||
const JKSessionScreen = () => {
|
||
const logger = console; // Replace with another logging mechanism if needed
|
||
const dispatch = useDispatch();
|
||
const app = useJamKazamApp();
|
||
const { currentUser } = useAuth();
|
||
|
||
const {
|
||
guardAgainstInvalidConfiguration,
|
||
guardAgainstActiveProfileMissing,
|
||
guardAgainstSinglePlayerProfile,
|
||
resyncAudio,
|
||
} = useGearUtils();
|
||
|
||
const { initialize: initializeMixer, onSessionChange } = useMixerStore();
|
||
const mixerHelper = useMixersContext();
|
||
const { isConnected,
|
||
ConnectionStatus,
|
||
connectionStatus,
|
||
reconnectAttempts,
|
||
lastError,
|
||
jamClient,
|
||
server,
|
||
registerMessageCallback,
|
||
unregisterMessageCallback } = useJamServerContext();
|
||
|
||
// Phase 4: Replace CurrentSessionContext with Redux
|
||
const currentSession = useSelector(selectActiveSession);
|
||
const sessionId = useSelector(selectSessionId);
|
||
const inSessionFlag = useSelector(selectInSession);
|
||
|
||
// Create ref for line 225, 365 compatibility
|
||
const currentSessionIdRef = useRef(sessionId);
|
||
useEffect(() => {
|
||
currentSessionIdRef.current = sessionId;
|
||
}, [sessionId]);
|
||
|
||
const inSession = useCallback(() => inSessionFlag, [inSessionFlag]);
|
||
|
||
const { globalObject, metronomeState, updateMetronomeState, closeMetronome, resetMetronome } = useGlobalContext();
|
||
const { getCurrentRecordingState, reset: resetRecordingState, currentlyRecording } = useRecordingHelpers();
|
||
const { SessionPageEnter } = useSessionUtils();
|
||
|
||
// Redux media state and actions
|
||
const mediaSummary = useSelector(selectMediaSummary);
|
||
const metronomeTrackMixers = useSelector(selectMetronomeTrackMixers);
|
||
const { openBackingTrack, openMetronome: openMetronomeAction, loadJamTrack, closeMedia } = useMediaActions();
|
||
|
||
// Use the session model hook
|
||
const sessionModel = useSessionModel(app, server, null); // sessionScreen is null for now
|
||
const sessionHelper = useSessionHelper();
|
||
const { id: sessionIdFromUrl } = useParams(); // Renamed to avoid conflict with Redux sessionId
|
||
const history = useHistory();
|
||
|
||
// Redux session lifecycle state
|
||
const activeSession = useSelector(selectActiveSession);
|
||
const joinStatus = useSelector(selectJoinStatus);
|
||
const hasJoined = useSelector(selectHasJoined);
|
||
const sessionGuardsPassed = useSelector(selectGuardsPassed);
|
||
const userTracks = useSelector(selectUserTracks);
|
||
const mixersReady = useSelector(selectMixersReady);
|
||
const showConnectionAlert = useSelector(selectShowConnectionAlert);
|
||
const reduxSessionId = useSelector(selectSessionId);
|
||
|
||
// Non-lifecycle state (keeping as local for now)
|
||
const [requestingSessionRefresh, setRequestingSessionRefresh] = useState(false);
|
||
const [pendingSessionRefresh, setPendingSessionRefresh] = useState(false);
|
||
|
||
// Redux modal state
|
||
const showSettingsModal = useSelector(selectModal('settings'));
|
||
const showInviteModal = useSelector(selectModal('invite'));
|
||
const showVolumeModal = useSelector(selectModal('volume'));
|
||
const showRecordingModal = useSelector(selectModal('recording'));
|
||
const showLeaveModal = useSelector(selectModal('leave'));
|
||
const showJamTrackModal = useSelector(selectModal('jamTrack'));
|
||
const showBackingTrackPopup = useSelector(selectModal('backingTrack'));
|
||
const showMediaControlsPopup = useSelector(selectModal('mediaControls'));
|
||
|
||
// Non-modal state for settings modal
|
||
const [settingsLoading, setSettingsLoading] = useState(false);
|
||
|
||
// Non-modal state for invite modal
|
||
const [friends, setFriends] = useState([]);
|
||
const [sessionInvitees, setSessionInvitees] = useState([]);
|
||
const [inviteLoading, setInviteLoading] = useState(false);
|
||
|
||
// Non-modal state for volume modal
|
||
const [volumeLevel, setVolumeLevel] = useState(100);
|
||
|
||
// Non-modal state for leave modal
|
||
const [leaveRating, setLeaveRating] = useState(null); // null, 'thumbsUp', 'thumbsDown'
|
||
const [leaveComments, setLeaveComments] = useState('');
|
||
const [leaveLoading, setLeaveLoading] = useState(false);
|
||
|
||
//state for video button
|
||
const [videoLoading, setVideoLoading] = useState(false);
|
||
|
||
// State for resync button
|
||
const [resyncLoading, setResyncLoading] = useState(false);
|
||
|
||
// Redux backing track state (modal visibility and data)
|
||
const backingTrackData = useSelector(selectBackingTrackData);
|
||
const showBackingTrackPlayer = Boolean(backingTrackData);
|
||
|
||
// Redux upload state for attachments
|
||
const isUploading = useSelector(selectIsUploading);
|
||
const uploadError = useSelector(selectUploadError);
|
||
const uploadFileName = useSelector(selectUploadFileName);
|
||
const uploadStatus = useSelector(selectUploadStatus);
|
||
const prevUploadStatusRef = useRef(uploadStatus);
|
||
|
||
// File input ref for attach button
|
||
const attachFileInputRef = useRef(null);
|
||
|
||
// Stable callback for backing track popup close
|
||
const handleBackingTrackClose = useCallback(() => {
|
||
console.log('JKSessionScreen: Backing Track Popup closing');
|
||
dispatch(closeModal('backingTrack'));
|
||
dispatch(clearBackingTrackData());
|
||
}, [dispatch]);
|
||
|
||
// Stable callback for backing track close in main screen
|
||
const handleBackingTrackMainClose = useCallback(async () => {
|
||
console.log('JKSessionScreen: Backing Track main screen close requested');
|
||
await closeMedia();
|
||
// Also clear the backing track popup data
|
||
dispatch(clearBackingTrackData());
|
||
dispatch(closeModal('backingTrack'));
|
||
}, [closeMedia, dispatch]);
|
||
|
||
// Redux JamTrack player state (modal visibility and data)
|
||
const jamTrackData = useSelector(selectJamTrackData);
|
||
const showJamTrackPlayer = Boolean(jamTrackData);
|
||
|
||
// Redux jam track state
|
||
const selectedJamTrack = useSelector(selectSelectedJamTrack);
|
||
const jamTrackStems = useSelector(selectJamTrackStems);
|
||
const jamTrackDownloadState = useSelector(state => state.media.downloadState);
|
||
|
||
// JamTrack close handler (used by both player and session screen)
|
||
const handleJamTrackClose = useCallback(async () => {
|
||
console.log('Closing jam track');
|
||
try {
|
||
// Call the close jam track API
|
||
await closeJamTrack({ id: currentSession.id });
|
||
|
||
// Clear the selected jam track and stems (session screen)
|
||
dispatch(setSelectedJamTrack(null));
|
||
dispatch(setJamTrackStems([]));
|
||
|
||
// Clear the player popup data
|
||
dispatch(clearJamTrackData());
|
||
|
||
toast.success('JamTrack closed successfully');
|
||
} catch (error) {
|
||
console.error('Error closing jam track:', error);
|
||
toast.error('Failed to close JamTrack');
|
||
}
|
||
}, [currentSession, dispatch]);
|
||
|
||
// Metronome close handler (used by both popup and session track)
|
||
const handleMetronomeClose = useCallback(async () => {
|
||
console.log('JKSessionScreen: Closing metronome');
|
||
try {
|
||
// Call jamClient to close metronome
|
||
await jamClient.SessionCloseMetronome();
|
||
|
||
// Update local metronome state
|
||
if (closeMetronome) {
|
||
closeMetronome();
|
||
}
|
||
|
||
toast.success('Metronome closed successfully');
|
||
} catch (error) {
|
||
console.error('Error closing metronome:', error);
|
||
toast.error('Failed to close metronome');
|
||
}
|
||
}, [jamClient, closeMetronome]);
|
||
|
||
// Stable callback for JamTrack player popup close (WindowPortal X button or ESC)
|
||
const handleJamTrackPlayerClose = useCallback(async () => {
|
||
console.log('JKSessionScreen: JamTrack Player Popup closing');
|
||
// Call the full close handler to clean up everything
|
||
await handleJamTrackClose();
|
||
}, [handleJamTrackClose]);
|
||
|
||
// State for media controls popup (modal visibility now in Redux)
|
||
const [mediaControlsOpened, setMediaControlsOpened] = useState(false);
|
||
const [popupGuard, setPopupGuard] = useState(false); // Hard guard against infinite loops
|
||
|
||
// Store references to registered message callbacks for cleanup
|
||
const [registeredCallbacks, setRegisteredCallbacks] = useState([]);
|
||
|
||
// Function to unregister message callbacks
|
||
const unregisterMessageCallbacks = useCallback(() => {
|
||
registeredCallbacks.forEach(({ type, callback }) => {
|
||
unregisterMessageCallback(type, callback);
|
||
});
|
||
setRegisteredCallbacks([]);
|
||
}, [registeredCallbacks, unregisterMessageCallback]);
|
||
|
||
// Fetch session data when URL sessionId changes
|
||
useEffect(() => {
|
||
if (sessionIdFromUrl && sessionIdFromUrl !== sessionId) {
|
||
console.log('Fetching active session:', sessionIdFromUrl);
|
||
dispatch(fetchActiveSession(sessionIdFromUrl));
|
||
}
|
||
}, [sessionIdFromUrl, sessionId, dispatch]);
|
||
|
||
// Setup global alert callback handler for backend mixer changes
|
||
useEffect(() => {
|
||
if (!window.JK) window.JK = {};
|
||
|
||
// Alert callback handler - routes backend alerts to appropriate handlers
|
||
const alertCallbackHandler = (type, text) => {
|
||
console.log('[AlertCallback] type:', type, 'text:', text);
|
||
// BACKEND_MIXER_CHANGE = 2
|
||
if (type === 2) {
|
||
console.log('[AlertCallback] BACKEND_MIXER_CHANGE:', text);
|
||
if (sessionModel && sessionModel.onBackendMixerChanged) {
|
||
sessionModel.onBackendMixerChanged(type, text);
|
||
}
|
||
}
|
||
// Alert code 40: Audio profile issues (common with built-in audio)
|
||
if (type === 40) {
|
||
console.warn('[AlertCallback] Audio profile alert (40):', text);
|
||
}
|
||
// Handle other alert types as needed
|
||
};
|
||
|
||
// Register under both names (native client may use either)
|
||
window.JK.HandleAlertCallback = alertCallbackHandler;
|
||
window.JK.AlertCallback = alertCallbackHandler;
|
||
|
||
return () => {
|
||
if (window.JK) {
|
||
delete window.JK.HandleAlertCallback;
|
||
}
|
||
};
|
||
}, [sessionModel]);
|
||
|
||
useEffect(() => {
|
||
if (!isConnected || !jamClient) return;
|
||
console.debug("-DEBUG- JKSessionScreen: isConnected changed to true");
|
||
|
||
guardOnJoinSession();
|
||
}, [isConnected, jamClient]); // Added jamClient to dependencies for stability
|
||
|
||
const guardOnJoinSession = async () => {
|
||
console.log("-DEBUG- JKSessionScreen: guardOnJoinSession")
|
||
try {
|
||
const musicSessionResp = await getSessionHistory(sessionIdFromUrl);
|
||
const musicSession = await musicSessionResp.json();
|
||
logger.log("-DEBUG- JKSessionScreen: fetched session history: ", musicSession);
|
||
dispatch(setSessionId(musicSession.id)); // Phase 4: dispatch to Redux
|
||
|
||
const musicianAccessOnJoin = musicSession.musician_access;
|
||
const shouldVerifyNetwork = musicSession.musician_access;
|
||
const clientRole = await jamClient.getClientParentChildRole();
|
||
|
||
logger.log("-DEBUG- JKSessionScreen: musicianAccessOnJoin when joining session: " + musicianAccessOnJoin);
|
||
logger.log("-DEBUG- JKSessionScreen: clientRole when joining session: " + clientRole);
|
||
logger.log("-DEBUG- JKSessionScreen: currentSessionId when joining session: " + currentSessionIdRef.current);
|
||
|
||
if (clientRole === CLIENT_ROLE.CHILD) {
|
||
logger.debug("-DEBUG- JKSessionScreen: client is configured to act as child. skipping all checks. assuming 0 tracks");
|
||
dispatch(setUserTracks([]));
|
||
|
||
//skipping all checks. assuming 0 tracks
|
||
await joinSession();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await guardAgainstInvalidConfiguration(app, shouldVerifyNetwork);
|
||
const result = await SessionPageEnter();
|
||
logger.log("-DEBUG- JKSessionScreen: SessionPageEnter result: ", result);
|
||
|
||
try {
|
||
await guardAgainstActiveProfileMissing(app, result);
|
||
logger.log("-DEBUG- JKSessionScreen: user has an active profile");
|
||
try {
|
||
const tracks = await sessionModel.waitForSessionPageEnterDone();
|
||
dispatch(setUserTracks(tracks));
|
||
logger.log("-DEBUG- JKSessionScreen: userTracks: ", tracks);
|
||
try {
|
||
await ensureAppropriateProfile(musicianAccessOnJoin)
|
||
logger.log("-DEBUG- JKSessionScreen: user has passed all session guards")
|
||
|
||
dispatch(setGuardsPassed(true))
|
||
|
||
} catch (error) {
|
||
logger.error("-DEBUG- JKSessionScreen: User profile is not appropriate for session:", error);
|
||
// If error doesn't control navigation, show alert (legacy behavior shows dialog)
|
||
if (!error?.controlled_location) {
|
||
logger.debug("-DEBUG- JKSessionScreen: Profile guard failed, showing alert");
|
||
// TODO: Replace with proper JKSessionProfileDialog component
|
||
alert(
|
||
'Your current audio profile is not suitable for multi-user sessions.\n\n' +
|
||
'Please configure a proper audio interface in your audio settings, ' +
|
||
'or create a private session for solo practice.'
|
||
);
|
||
await sessionModel.handleLeaveSession();
|
||
history.push('/'); // Redirect to dashboard
|
||
return; // Don't continue with session join
|
||
}
|
||
// If controlled_location is true, the error handler already redirected
|
||
}
|
||
} catch (error) {
|
||
logger.error("-DEBUG- JKSessionScreen: Error: waiting for session page enter to complete:", error);
|
||
if (error === "timeout") {
|
||
//TODO: show some error
|
||
} else if (error === 'session_over') {
|
||
// do nothing; session ended before we got the user track info. just bail
|
||
logger.debug("-DEBUG- JKSessionScreen: Error:: session is over; bailing");
|
||
} else {
|
||
//TODO: show some error
|
||
}
|
||
|
||
await sessionModel.handleLeaveSession(); //TODO: handle redirection
|
||
}
|
||
} catch (error) {
|
||
// Active profile is missing, redirect to home or if the error has a location, redirect there
|
||
logger.error("-DEBUG- JKSessionScreen: Error: Active profile is missing or invalid:", error);
|
||
}
|
||
} catch (error) {
|
||
// Invalid configuration, redirect to home
|
||
await sessionModel.handleLeaveSession(); //TODO: handle redirection
|
||
logger.error("-DEBUG- JKSessionScreen: Error: Invalid configuration:", error);
|
||
}
|
||
|
||
} catch (error) {
|
||
logger.error("-DEBUG- JKSessionScreen: Error: Error fetching session history:", error);
|
||
//TODO: Show some error
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (!sessionGuardsPassed || userTracks.length === 0 || hasJoined) { return }
|
||
joinSession();
|
||
}, [sessionGuardsPassed, userTracks, hasJoined])
|
||
|
||
// Track sync: Sync tracks to server when session joined (3-call pattern matching legacy)
|
||
// IMPORTANT: Wait for mixers to be ready before syncing to avoid race condition with mixer initialization
|
||
useEffect(() => {
|
||
if (!hasJoined || !sessionId || !server?.clientId || !mixersReady) {
|
||
return;
|
||
}
|
||
|
||
console.log('[Track Sync] Mixers ready, scheduling track sync calls');
|
||
|
||
// First sync: Initial setup (~1s after join)
|
||
const timer1 = setTimeout(() => {
|
||
dispatch(syncTracksToServer(sessionId, server.clientId));
|
||
}, 1000);
|
||
|
||
// Second sync: Refinement (~1.4s after join)
|
||
const timer2 = setTimeout(() => {
|
||
dispatch(syncTracksToServer(sessionId, server.clientId));
|
||
}, 1400);
|
||
|
||
// Third sync: Final config (~6s after join)
|
||
const timer3 = setTimeout(() => {
|
||
dispatch(syncTracksToServer(sessionId, server.clientId));
|
||
}, 6000);
|
||
|
||
// Cleanup timers on unmount or if hasJoined/sessionId changes
|
||
return () => {
|
||
clearTimeout(timer1);
|
||
clearTimeout(timer2);
|
||
clearTimeout(timer3);
|
||
};
|
||
// Note: server intentionally NOT in deps to avoid re-running on server reference changes
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [hasJoined, sessionId, mixersReady, dispatch])
|
||
|
||
// Fetch chat history when session joins to populate unread badge
|
||
// This ensures unread count persists across page reloads
|
||
useEffect(() => {
|
||
if (!hasJoined || !sessionId) {
|
||
return;
|
||
}
|
||
|
||
dispatch(fetchChatHistory({
|
||
channel: sessionId,
|
||
sessionId: sessionId
|
||
}));
|
||
}, [hasJoined, sessionId, dispatch]);
|
||
|
||
const joinSession = async () => {
|
||
|
||
await jamClient.SetVURefreshRate(150);
|
||
await jamClient.SessionRegisterCallback("JK.HandleBridgeCallback2");
|
||
await jamClient.SessionSetAlertCallback("JK.HandleAlertCallback");
|
||
await jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted");
|
||
|
||
await jamClient.SessionSetConnectionStatusRefreshRate(1000);
|
||
|
||
let clientRole = await jamClient.getClientParentChildRole();
|
||
const parentClientId = await jamClient.getParentClientId();
|
||
console.debug('role when joining session: ' + clientRole + ', parentClientId: ' + parentClientId);
|
||
|
||
|
||
|
||
if (clientRole === 0) {
|
||
clientRole = 'child';
|
||
} else if (clientRole === 1) {
|
||
clientRole = 'parent';
|
||
}
|
||
|
||
if ((clientRole === '') || !clientRole) {
|
||
clientRole = null;
|
||
}
|
||
|
||
// subscribe to events from the recording model
|
||
//this.recordingRegistration(); //TODO: implement recording registration
|
||
|
||
// tell the server we want to join
|
||
|
||
|
||
|
||
//const clientId = await jamClient.clientID();
|
||
const clientId = server.clientId;
|
||
console.log("joining session " + sessionId + " as client " + JSON.stringify(clientId) + " with role " + clientRole + " and parent client " + parentClientId);
|
||
|
||
const latency = await jamClient.FTUEGetExpectedLatency().latency
|
||
|
||
console.log("joinSession parameters: ", {
|
||
client_id: clientId,
|
||
ip_address: server.publicIP,
|
||
as_musician: true,
|
||
tracks: userTracks,
|
||
session_id: sessionId,
|
||
client_role: clientRole,
|
||
parent_client_id: parentClientId,
|
||
audio_latency: latency,
|
||
});
|
||
|
||
joinSessionRest({
|
||
client_id: clientId,
|
||
ip_address: server.publicIP,
|
||
as_musician: true,
|
||
tracks: userTracks,
|
||
session_id: sessionId,
|
||
client_role: clientRole,
|
||
parent_client_id: parentClientId,
|
||
audio_latency: latency,
|
||
}).then(async (response) => {
|
||
console.debug("joinSessionRest response received", response.errors);
|
||
if (response.errors) {
|
||
throw new Error("Unable to join session: " + JSON.stringify(response.errors));
|
||
} else {
|
||
|
||
const data = await response.json();
|
||
console.debug("join session response xxx: ", data);
|
||
|
||
// Update Redux state - user has successfully joined
|
||
dispatch(joinActiveSession.fulfilled(data, '', { sessionId, options: {} }));
|
||
|
||
if (!inSession()) {
|
||
// the user has left the session before they got joined. We need to issue a leave again to the server to make sure they are out
|
||
logger.debug("user left before fully joined to session. telling server again that they have left");
|
||
sessionModel.leaveSessionRest();
|
||
}
|
||
|
||
sessionModel.updateSessionInfo(data, true);
|
||
dispatch(updateSessionData(data)); // Phase 4: dispatch to Redux
|
||
|
||
//TODO: revist this logic later
|
||
// on temporary disconnect scenarios, a user may already be in a session when they enter this path
|
||
// so we avoid double counting
|
||
// if (!this.alreadyInSession()) {
|
||
// if (this.participants().length === 1) {
|
||
// context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.create);
|
||
// } else {
|
||
// context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.join);
|
||
// }
|
||
// }
|
||
|
||
// this.recordingModel.reset(this.currentSessionId); //TODO: implement recording model
|
||
|
||
const joinSessionMsg = {
|
||
sessionID: currentSession.id,
|
||
music_session_id_int: response.music_session_id_int
|
||
};
|
||
|
||
await jamClient.JoinSession(joinSessionMsg);
|
||
|
||
//@refreshCurrentSession(true);
|
||
|
||
// Chat message handler - receives messages from WebSocket and dispatches to Redux
|
||
// Callback signature: (fullMessage, payload) where payload contains the chat_message data
|
||
const handleChatMessage = (fullMessage, payload) => {
|
||
// Transform Protocol Buffer format to Redux format
|
||
// payload contains: {sender_name, sender_id, msg, msg_id, created_at, channel, ...}
|
||
const message = {
|
||
id: payload.msg_id,
|
||
senderId: payload.sender_id,
|
||
senderName: payload.sender_name,
|
||
message: payload.msg,
|
||
createdAt: payload.created_at,
|
||
channel: payload.channel || 'session',
|
||
sessionId: currentSession.id,
|
||
// Attachment fields (null/undefined if not an attachment)
|
||
purpose: payload.purpose, // 'Notation File', 'Audio File', or undefined
|
||
attachmentId: payload.attachment_id, // MusicNotation UUID
|
||
attachmentType: payload.attachment_type, // 'notation' or 'audio'
|
||
attachmentName: payload.attachment_name, // filename
|
||
attachmentSize: payload.attachment_size // file size in bytes (may be null)
|
||
};
|
||
dispatch(addMessageFromWebSocket(message));
|
||
};
|
||
|
||
// Register message callbacks and store references for cleanup
|
||
const callbacksToRegister = [
|
||
{ type: MessageType.SESSION_JOIN, callback: trackChanges },
|
||
{ type: MessageType.SESSION_DEPART, callback: trackChanges },
|
||
{ type: MessageType.TRACKS_CHANGED, callback: trackChanges },
|
||
{ type: MessageType.HEARTBEAT_ACK, callback: trackChanges },
|
||
{ type: MessageType.CHAT_MESSAGE, callback: handleChatMessage }
|
||
];
|
||
|
||
callbacksToRegister.forEach(({ type, callback }) => {
|
||
registerMessageCallback(type, callback);
|
||
});
|
||
|
||
// Store registered callbacks for cleanup
|
||
setRegisteredCallbacks(callbacksToRegister);
|
||
|
||
//TODO: revist the logic in following commented section
|
||
//if (document) { $(document).trigger(EVENTS.SESSION_STARTED, { session: { id: this.currentSessionId, lesson_session: response.lesson_session } }); }
|
||
|
||
// this.handleAutoOpenJamTrack();
|
||
|
||
// this.watchBackendStats();
|
||
|
||
// ConfigureTracksActions.reset(true);
|
||
// this.delayEnableVst();
|
||
// logger.debug("completed session join")
|
||
|
||
}
|
||
|
||
}).catch((xhr) => {
|
||
console.error("joinSessionRest error: ", xhr);
|
||
let leaveBehavior;
|
||
sessionModel.updateCurrentSession(null);
|
||
|
||
if (xhr.status === 404) {
|
||
// we tried to join the session, but it is already gone. kick user back to join session screen
|
||
|
||
} else if (xhr.status === 422) {
|
||
//console.error("unable to join session - 422 error");
|
||
// const response = JSON.parse(xhr.responseText);
|
||
// if (response["errors"] && response["errors"]["tracks"] && (response["errors"]["tracks"][0] === "Please select at least one track")) {
|
||
// // You will need to reconfigure your audio device. show an alert
|
||
// } else if (response["errors"] && response["errors"]["music_session"] && (response["errors"]["music_session"][0] == ["is currently recording"])) {
|
||
// //The session is currently recording. You can not join a session that is recording. show an alert
|
||
// } else if (response["errors"] && response["errors"]["remaining_session_play_time"]) {
|
||
// //user has no available playtime. upgrade
|
||
// } else if (response["errors"] && response["errors"]["remaining_month_play_time"]) {
|
||
// //user has no available playtime. upgrade
|
||
// } else {
|
||
// // unknown 422 error. alert unable to join sessio
|
||
// }
|
||
} else {
|
||
// unable to join session
|
||
}
|
||
})
|
||
|
||
}
|
||
|
||
useEffect(() => {
|
||
if (!isConnected || !hasJoined) return;
|
||
onSessionChange(sessionHelper);
|
||
}, [isConnected, hasJoined, sessionHelper.id()]);
|
||
|
||
const ensureAppropriateProfile = async (musicianAccess) => {
|
||
return new Promise(async function (resolve, reject) {
|
||
if (musicianAccess) {
|
||
try {
|
||
await guardAgainstSinglePlayerProfile(app);
|
||
resolve();
|
||
} catch (error) {
|
||
reject(error)
|
||
}
|
||
} else {
|
||
resolve();
|
||
}
|
||
})
|
||
};
|
||
|
||
// Use sessionModel functions
|
||
const trackChanges = sessionModel.trackChanges;
|
||
const refreshCurrentSession = sessionModel.refreshCurrentSession;
|
||
const updateSession = sessionModel.updateSession;
|
||
|
||
const musicianAccess = useMemo(() => {
|
||
if (!currentSession) return null;
|
||
return sessionModel.getMusicianAccess();
|
||
}, [currentSession]);
|
||
|
||
// Memoize chat object to prevent unnecessary re-renders
|
||
const chat = useMemo(() => {
|
||
if (!mixerHelper.chatMixer) return null;
|
||
return {
|
||
mixers: mixerHelper.chatMixer,
|
||
mode: mixerHelper.mixMode,
|
||
photoUrl: mixerHelper.myTracks.length > 0 ? mixerHelper.myTracks[0].photoUrl : '',
|
||
name: 'Chat'
|
||
};
|
||
}, [mixerHelper.chatMixer, mixerHelper.mixMode, mixerHelper.myTracks]);
|
||
|
||
// useEffect(() => {
|
||
// if (!isConnected) return;
|
||
// // validate session by fetching the session from the server
|
||
// fetchSession();
|
||
// }, [isConnected]);
|
||
|
||
// const fetchSession = async () => {
|
||
// const session = await getSession(sessionId);
|
||
// if (session) {
|
||
// setSessionState(prevState => ({
|
||
// ...prevState,
|
||
// ...session
|
||
// }));
|
||
// } else {
|
||
// logger.error("Invalid session ID or unable to fetch session");
|
||
// //TODO: Handle invalid session (e.g., redirect or show error)
|
||
// }
|
||
// };
|
||
|
||
// Monitor connection status changes
|
||
useEffect(() => {
|
||
if (connectionStatus === ConnectionStatus.DISCONNECTED ||
|
||
connectionStatus === ConnectionStatus.ERROR) {
|
||
dispatch(setConnectionStatus('disconnected'));
|
||
} else if (connectionStatus === ConnectionStatus.CONNECTED) {
|
||
dispatch(setConnectionStatus('connected'));
|
||
} else if (connectionStatus === ConnectionStatus.RECONNECTING) {
|
||
dispatch(setConnectionStatus('reconnecting'));
|
||
}
|
||
}, [connectionStatus, dispatch]);
|
||
|
||
// Handlers for recording and playback
|
||
// const handleStartRecording = () => {
|
||
// jamClient.StartRecording({ recordingId: `rec_${Date.now()}` });
|
||
// };
|
||
|
||
// const handleStopRecording = () => {
|
||
// jamClient.StopRecording({});
|
||
// };
|
||
|
||
// const handlePlayPause = () => {
|
||
// if (isPlaying) {
|
||
// jamClient.SessionPausePlay();
|
||
// } else {
|
||
// jamClient.SessionStartPlay();
|
||
// }
|
||
// };
|
||
|
||
// const handleStopPlayback = () => {
|
||
// jamClient.SessionStopPlay();
|
||
// };
|
||
|
||
// // Callback handlers (these would be implemented to handle WebSocket responses)
|
||
// const HandleSessionCallback = (data) => {
|
||
// logger.log('Session callback:', data);
|
||
// // Handle session events
|
||
// };
|
||
|
||
// const HandleRecordingStarted = (data) => {
|
||
// logger.log('Recording started:', data);
|
||
// // Update recording state
|
||
// };
|
||
|
||
// const HandleRecordingStopped = (data) => {
|
||
// logger.log('Recording stopped:', data);
|
||
// // Update recording state
|
||
// };
|
||
|
||
// const HandleVolumeChangeCallback = (mixerId, isLeft, value, isMuted) => {
|
||
// logger.log('Volume changed:', { mixerId, isLeft, value, isMuted });
|
||
// // Update mixer state
|
||
// };
|
||
|
||
// const HandleBridgeCallback = (vuData) => {
|
||
// logger.log('Bridge callback:', vuData);
|
||
// // Handle VU meter updates
|
||
// };
|
||
|
||
|
||
|
||
useEffect(() => {
|
||
fetchFriends();
|
||
}, []);
|
||
|
||
const fetchFriends = () => {
|
||
if (currentUser) {
|
||
getFriends(currentUser.id)
|
||
.then(resp => {
|
||
if (resp.ok) {
|
||
return resp.json();
|
||
}
|
||
})
|
||
.then(data => {
|
||
setFriends(data);
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleRecordingSubmit = async (settings) => {
|
||
settings.volume = getCurrentRecordingState().inputVolumeLevel;
|
||
try {
|
||
localStorage.setItem("recordSettings", JSON.stringify(settings));
|
||
} catch (e) {
|
||
logger.info("error while saving recordSettings to localStorage");
|
||
logger.log(e.stack);
|
||
}
|
||
|
||
const params = {
|
||
recordingType: settings.recordingType,
|
||
name: settings.recordingName,
|
||
audioFormat: settings.audioFormat,
|
||
audioStoreType: settings.audioStoreType,
|
||
includeChat: settings.includeChat,
|
||
volume: settings.volume,
|
||
};
|
||
|
||
if (params.recordingType === RECORD_TYPE_BOTH) {
|
||
params['videoFormat'] = settings.videoFormat;
|
||
params['audioDelay'] = settings.audioDelay;
|
||
|
||
const obsAvailable = await jamClient.IsOBSAvailable();
|
||
|
||
if (!obsAvailable) {
|
||
toast.warning("OBS Studio is not available. Please ensure OBS Studio is installed and running to record video.");
|
||
return;
|
||
}
|
||
|
||
if (!globalObject.JK.videoIsOngoing) {
|
||
toast.warning("To make a video recording in JamKazam you must have an ongoing video. You can start a video by clicking the Video button on session tool bar.");
|
||
return;
|
||
}
|
||
}
|
||
//this.startStopRecording(params);
|
||
//TODO: handle startStopRecording
|
||
doStartRecording(params);
|
||
|
||
}
|
||
|
||
function groupTracksToClient(recording) {
|
||
// group N tracks to the same client Id
|
||
let groupedTracks = {};
|
||
let recordingTracks = recording["recorded_tracks"];
|
||
for (let i = 0; i < recordingTracks.length; i++) {
|
||
let clientId = recordingTracks[i].client_id;
|
||
|
||
let tracksForClient = groupedTracks[clientId];
|
||
if (!tracksForClient) {
|
||
tracksForClient = [];
|
||
groupedTracks[clientId] = tracksForClient;
|
||
}
|
||
tracksForClient.push(recordingTracks[i]);
|
||
}
|
||
return dkeys(groupedTracks);
|
||
}
|
||
|
||
const doStartRecording = (params) => {
|
||
startRecording({ music_session_id: currentSession.id, recordVideo: params.recordVideo }).then(async (recording) => {
|
||
const currentRecordingId = recording.id;
|
||
console.debug("Recording started with ID: ", currentRecordingId);
|
||
const groupedTracks = groupTracksToClient(recording);
|
||
try {
|
||
await jamClient.StartMediaRecording(currentRecordingId, groupedTracks, params);
|
||
} catch (error) {
|
||
console.error("Error starting media recording:", error);
|
||
}
|
||
}).catch((error) => {
|
||
console.error("Error starting recording:", error);
|
||
});
|
||
}
|
||
|
||
const handleLeaveSession = () => {
|
||
// Just show the modal - no leave operations yet
|
||
dispatch(openModal('leave'));
|
||
};
|
||
|
||
const handleLeaveSubmit = async (feedbackData) => {
|
||
try {
|
||
setLeaveLoading(true);
|
||
|
||
// Unregister message callbacks before leaving
|
||
unregisterMessageCallbacks();
|
||
|
||
// Close metronome if open before leaving
|
||
if (metronomeState.isOpen) {
|
||
console.log('Closing metronome before leaving session');
|
||
closeMetronome();
|
||
}
|
||
|
||
// Submit feedback to backend first
|
||
const clientId = server.clientId;
|
||
const backendDetails = jamClient.getAllClientsStateMap ? jamClient.getAllClientsStateMap() : {};
|
||
|
||
await submitSessionFeedback(clientId, {
|
||
rating: feedbackData.rating,
|
||
comment: feedbackData.comments,
|
||
backend_details: backendDetails
|
||
});
|
||
|
||
// Then perform leave operations
|
||
await sessionModel.handleLeaveSession();
|
||
|
||
// Clear Redux session state
|
||
dispatch(clearSession());
|
||
|
||
dispatch(closeModal('leave'));
|
||
toast.success('Thank you for your feedback!');
|
||
|
||
// Navigate to sessions page using React Router
|
||
history.push('/sessions');
|
||
} catch (error) {
|
||
console.error('Error submitting feedback or leaving session:', error);
|
||
toast.error('Failed to submit feedback or leave session');
|
||
} finally {
|
||
setLeaveLoading(false);
|
||
}
|
||
};
|
||
|
||
// Cleanup metronome state and message callbacks when component unmounts
|
||
useEffect(() => {
|
||
return () => {
|
||
// Unregister message callbacks
|
||
unregisterMessageCallbacks();
|
||
|
||
// Reset metronome state when component unmounts (session ends)
|
||
if (metronomeState.isOpen) {
|
||
console.log('Resetting metronome state on session cleanup');
|
||
resetMetronome();
|
||
}
|
||
|
||
// Clear Redux session state on unmount
|
||
dispatch(clearSession());
|
||
};
|
||
}, [metronomeState.isOpen, resetMetronome, dispatch]);
|
||
|
||
// Check if user can use video (subscription/permission check)
|
||
const canVideo = () => {
|
||
// This would need to be implemented based on user subscription logic
|
||
console.debug("JKSessionScreen: Checking video permission for user", currentSession);
|
||
return currentSession?.can_use_video || false;
|
||
};
|
||
|
||
// Open external link in new window/tab
|
||
const openExternalLink = (url) => {
|
||
window.open(url, '_blank', 'noopener,noreferrer');
|
||
};
|
||
|
||
// Handle video button click - opens new video conferencing server
|
||
const handleVideoClick = async () => {
|
||
if (!canVideo()) {
|
||
// Show upgrade modal/banner
|
||
showVideoUpgradePrompt();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setVideoLoading(true);
|
||
|
||
// Get video conferencing room URL from server
|
||
const response = await getVideoConferencingRoomUrl(currentSession.id);
|
||
const videoUrl = `${response.url}&audiooff=true`;
|
||
|
||
// Open video URL in new browser window/tab
|
||
console.debug("JKSessionScreen: Opening video conferencing URL", videoUrl);
|
||
openExternalLink(videoUrl);
|
||
|
||
} catch (error) {
|
||
console.error('Failed to get video room URL:', error);
|
||
// Handle error - could show error message to user
|
||
toast.error('Failed to start video session');
|
||
} finally {
|
||
// Keep loading state for 10 seconds to prevent multiple clicks
|
||
setTimeout(() => setVideoLoading(false), 10000);
|
||
}
|
||
};
|
||
|
||
// Show upgrade prompt for users without video permissions
|
||
const showVideoUpgradePrompt = () => {
|
||
// Implementation for showing upgrade modal/banner
|
||
// This could use a modal context or toast notification
|
||
console.log('Show video upgrade prompt');
|
||
toast.warning(<VideoUpgradeContent />, { autoClose: false });
|
||
};
|
||
|
||
const VideoUpgradeContent = () => (
|
||
<div>
|
||
<h5>Upgrade Required</h5>
|
||
<p>The video feature requires a premium subscription. Please upgrade your plan to access video conferencing.</p>
|
||
<Button color="primary" onClick={() => {
|
||
// Redirect to upgrade page
|
||
history.push('/upgrade');
|
||
}}>
|
||
Upgrade Now
|
||
</Button>
|
||
</div>
|
||
);
|
||
|
||
const handleBroadcast = async (e) => {
|
||
e.preventDefault();
|
||
try {
|
||
await jamClient.LaunchBroadcastSettings();
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
};
|
||
|
||
// Handle Resync button click - performs audio resync via native client
|
||
const handleResync = useCallback(async (e) => {
|
||
e.preventDefault();
|
||
if (resyncLoading) return;
|
||
|
||
setResyncLoading(true);
|
||
try {
|
||
await resyncAudio();
|
||
// Silent success (matches legacy behavior)
|
||
} catch (error) {
|
||
console.error('Audio resync failed:', error);
|
||
if (error.message === 'timeout') {
|
||
toast.error('Audio resync timed out. Please try again.');
|
||
} else {
|
||
toast.error('Audio resync failed: ' + (error.message || 'Unknown error'));
|
||
}
|
||
} finally {
|
||
setResyncLoading(false);
|
||
}
|
||
}, [resyncAudio, resyncLoading]);
|
||
|
||
// Attach button handlers
|
||
const handleAttachClick = useCallback(() => {
|
||
if (attachFileInputRef.current) {
|
||
attachFileInputRef.current.click();
|
||
}
|
||
}, []);
|
||
|
||
const handleFileSelect = useCallback((e) => {
|
||
const file = e.target.files?.[0];
|
||
if (!file) return;
|
||
|
||
// Reset input for re-selection of same file
|
||
e.target.value = '';
|
||
|
||
// Validate file before upload
|
||
const validation = validateFile(file);
|
||
|
||
if (!validation.valid) {
|
||
toast.error(validation.error);
|
||
return;
|
||
}
|
||
|
||
// Show warnings if any (e.g., backend may not fully support this type)
|
||
if (validation.warnings) {
|
||
validation.warnings.forEach(warning => {
|
||
console.warn('Attachment warning:', warning);
|
||
});
|
||
}
|
||
|
||
// Open chat window if not already open
|
||
dispatch(openModal('chat'));
|
||
|
||
// 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,
|
||
userId: currentUser?.id,
|
||
userName: currentUser?.name
|
||
}));
|
||
}, [dispatch, sessionId, server?.clientId, currentUser?.id, currentUser?.name]);
|
||
|
||
// Show error toast when upload fails
|
||
useEffect(() => {
|
||
if (uploadError) {
|
||
toast.error(uploadError);
|
||
dispatch(clearUploadError());
|
||
}
|
||
}, [uploadError, dispatch]);
|
||
|
||
// Show success toast when upload completes
|
||
useEffect(() => {
|
||
// Show success toast when upload transitions from 'uploading' to 'idle'
|
||
if (prevUploadStatusRef.current === 'uploading' && uploadStatus === 'idle') {
|
||
toast.success('File uploaded successfully', { autoClose: 3000 });
|
||
}
|
||
prevUploadStatusRef.current = uploadStatus;
|
||
}, [uploadStatus]);
|
||
|
||
const handleBackingTrackSelected = async (result) => {
|
||
console.log('JKSessionScreen: handleBackingTrackSelected called with:', result);
|
||
console.log('JKSessionScreen: Current state - showBackingTrackPopup:', showBackingTrackPopup, 'popupGuard:', popupGuard);
|
||
|
||
try {
|
||
console.log('JKSessionScreen: Calling jamClient.SessionOpenBackingTrackFile...');
|
||
// Open the backing track file
|
||
await jamClient.SessionOpenBackingTrackFile(result.file, false);
|
||
console.log('JKSessionScreen: jamClient.SessionOpenBackingTrackFile completed');
|
||
|
||
// Set up data for the popup (don't store jamClient in Redux - it's not serializable)
|
||
console.log('JKSessionScreen: Setting backing track data...');
|
||
dispatch(setBackingTrackData({
|
||
backingTrack: result.file,
|
||
session: currentSession,
|
||
currentUser: currentUser
|
||
}));
|
||
|
||
// Show the popup
|
||
console.log('JKSessionScreen: Setting showBackingTrackPopup to true...');
|
||
dispatch(openModal('backingTrack'));
|
||
console.log('JKSessionScreen: handleBackingTrackSelected completed successfully');
|
||
//TODO: In the legacy client, the popup window was opened as a native window through the client. decide whether we need to replicate that behavior here or do it through the browser only
|
||
} catch (error) {
|
||
console.error('JKSessionScreen: Error opening backing track:', error);
|
||
toast.error('Failed to open backing track');
|
||
}
|
||
};
|
||
|
||
const handleJamTrackSelect = async (jamTrack) => {
|
||
console.log('Jam track selected:', jamTrack);
|
||
try {
|
||
// Fetch jam track details with stems
|
||
const response = await getJamTrack({ id: jamTrack.id });
|
||
const jamTrackWithStems = await response.json();
|
||
|
||
console.log('Jam track data:', jamTrackWithStems);
|
||
|
||
// Set the selected jam track and stems (for display on session screen)
|
||
dispatch(setSelectedJamTrack(jamTrackWithStems));
|
||
dispatch(setJamTrackStems(jamTrackWithStems.tracks || []));
|
||
|
||
// Open the JamTrack player popup (with full data needed for player)
|
||
dispatch(setJamTrackData({
|
||
jamTrack: jamTrackWithStems,
|
||
session: currentSession,
|
||
currentUser: currentUser
|
||
}));
|
||
|
||
toast.success(`Loaded JamTrack: ${jamTrackWithStems.name}`);
|
||
} catch (error) {
|
||
console.error('Error loading jam track:', error);
|
||
toast.error('Failed to load JamTrack');
|
||
}
|
||
};
|
||
|
||
const handleMetronomeSelected = async () => {
|
||
console.log('Opening metronome');
|
||
try {
|
||
// Check if currently recording - can't open metronome while recording
|
||
if (currentlyRecording) {
|
||
toast.warning("You can't open a metronome while recording.");
|
||
return;
|
||
}
|
||
|
||
// Check for unstable NTP clocks (like legacy implementation)
|
||
const unstableClocks = await checkUnstableClocks();
|
||
if (currentSession.participants && currentSession.participants.length > 1 && unstableClocks.length > 0) {
|
||
const names = unstableClocks.join(", ");
|
||
toast.warning(`Couldn't open metronome due to unstable clocks: ${names}`);
|
||
return;
|
||
}
|
||
|
||
// Track analytics (like legacy SessionStore)
|
||
if (window.stats && window.stats.write) {
|
||
const data = {
|
||
value: 1,
|
||
session_size: currentSession.participants?.length || 1,
|
||
user_id: currentUser?.id,
|
||
user_name: currentUser?.name
|
||
};
|
||
window.stats.write('web.metronome.open', data);
|
||
}
|
||
|
||
// Stop any current playback first (like legacy MixerStore)
|
||
await jamClient.SessionStopPlay();
|
||
|
||
// Default metronome settings for the controls
|
||
const bpm = 120;
|
||
const sound = 2; // Beep (numeric value)
|
||
const soundName = "Beep"; // String name for native client
|
||
const meter = 1;
|
||
const cricket = false;
|
||
|
||
console.log(`Opening metronome controls with default settings: bpm=${bpm}, sound=${soundName}, meter=${meter}`);
|
||
|
||
// Inform server about metronome opening (like legacy SessionStore)
|
||
await openMetronome({ id: currentSession.id });
|
||
|
||
// Initialize metronome track (creates mixers) then stop playback
|
||
// This allows the track to appear in the UI while audio remains stopped
|
||
// Use SessionStopPlay instead of SessionCloseMetronome to preserve metronome state
|
||
await jamClient.SessionOpenMetronome(bpm, soundName, meter, 0);
|
||
await jamClient.SessionStopPlay();
|
||
|
||
// Update local metronome state to show popup immediately
|
||
// Metronome track will be visible but audio is stopped (user must click Play)
|
||
if (updateMetronomeState) {
|
||
updateMetronomeState({
|
||
isOpen: true,
|
||
bpm: bpm,
|
||
sound: sound,
|
||
meter: meter,
|
||
cricket: cricket
|
||
});
|
||
}
|
||
|
||
toast.success('Metronome opened successfully');
|
||
} catch (error) {
|
||
console.error('Error opening metronome:', error);
|
||
toast.error('Failed to open metronome');
|
||
}
|
||
};
|
||
|
||
const checkUnstableClocks = async () => {
|
||
try {
|
||
const unstable = [];
|
||
|
||
// Check current user's NTP stability
|
||
const myState = await jamClient.getMyNetworkState();
|
||
if (!myState.ntp_stable) {
|
||
unstable.push('this computer');
|
||
}
|
||
|
||
// Check other participants' NTP stability
|
||
if (currentSession.participants) {
|
||
for (const participant of currentSession.participants) {
|
||
if (participant.client_id !== server.clientId) {
|
||
try {
|
||
const peerState = await jamClient.getPeerState(participant.client_id);
|
||
if (!peerState.ntp_stable) {
|
||
unstable.push(participant.user.first_name + ' ' + participant.user.last_name);
|
||
}
|
||
} catch (error) {
|
||
// Ignore errors for individual peer checks
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return unstable;
|
||
} catch (error) {
|
||
console.error('Error checking NTP stability:', error);
|
||
return [];
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Card>
|
||
{!isConnected && <div className='d-flex align-items-center'>Connecting to backend...</div>}
|
||
<FalconCardHeader title="Session" titleClass="font-weight-bold">
|
||
<Button color="primary" size="md" onClick={handleLeaveSession}>Leave Session</Button>
|
||
</FalconCardHeader>
|
||
|
||
<CardHeader className="bg-light border-bottom border-top py-2 border-3">
|
||
<div className="d-flex flex-nowrap overflow-auto" style={{ gap: '0.5rem', zIndex: 1100 }}>
|
||
<Button className='btn-custom-outline' outline size="md" onClick={() => dispatch(openModal('settings'))}>
|
||
<img src={gearIcon} alt="Settings" style={{ width: '16px', height: '16px', marginRight: '0.2rem' }} />
|
||
Settings</Button>
|
||
<Button className='btn-custom-outline' outline size="md" onClick={() => dispatch(openModal('invite'))}>
|
||
<img src={inviteIcon} alt="Invite" style={{ width: '16px', height: '16px', marginRight: '0.2rem' }} />
|
||
Invite</Button>
|
||
<Button className='btn-custom-outline' outline size="md" onClick={() => dispatch(openModal('volume'))}>
|
||
<img src={volumeIcon} alt="Volume" style={{ width: '16px', height: '16px', marginRight: '0.2rem' }} />
|
||
Volume</Button>
|
||
<Button className='btn-custom-outline' outline size="md" onClick={handleVideoClick} disabled={videoLoading}>
|
||
<img src={videoIcon} alt="Video" style={{ width: '16px', height: '16px', marginRight: '0.2rem' }} />
|
||
{videoLoading && (<Spinner size="sm" />)}
|
||
Video
|
||
</Button>
|
||
<Button className='btn-custom-outline' outline size="md" onClick={() => dispatch(openModal('recording'))}>
|
||
<img src={recordIcon} alt="Record" style={{ width: '16px', height: '16px', marginRight: '0.2rem' }} />
|
||
Record</Button>
|
||
<Button className='btn-custom-outline' outline size="md" onClick={ handleBroadcast}>
|
||
<img src={broadcastIcon} alt="Broadcast" style={{ width: '16px', height: '16px', marginRight: '0.2rem' }} />
|
||
Broadcast</Button>
|
||
<JKSessionOpenMenu onBackingTrackSelected={handleBackingTrackSelected} onJamTrackSelected={() => dispatch(openModal('jamTrack'))} onMetronomeSelected={handleMetronomeSelected} />
|
||
<JKSessionChatButton sessionId={sessionId} />
|
||
<input
|
||
type="file"
|
||
ref={attachFileInputRef}
|
||
style={{ display: 'none' }}
|
||
accept=".pdf,.xml,.mxl,.txt,.png,.jpg,.jpeg,.gif,.mp3,.wav"
|
||
onChange={handleFileSelect}
|
||
/>
|
||
<Button
|
||
className='btn-custom-outline'
|
||
outline
|
||
size="md"
|
||
onClick={handleAttachClick}
|
||
disabled={isUploading}
|
||
>
|
||
<img src={attachIcon} alt="Attach" style={{ width: '16px', height: '16px', marginRight: '0.2rem' }} />
|
||
{isUploading ? 'Uploading...' : 'Attach'}
|
||
</Button>
|
||
<Button className='btn-custom-outline' outline size="md" onClick={handleResync} disabled={resyncLoading}>
|
||
<img src={resyncIcon} alt="Resync" style={{ width: '16px', height: '16px', marginRight: '0.2rem' }} />
|
||
{resyncLoading ? <><Spinner size="sm" /> Resyncing...</> : 'Resync'}
|
||
</Button>
|
||
</div>
|
||
</CardHeader>
|
||
|
||
<CardBody className="pl-4" style={{ backgroundColor: '#edf2f9f5', overflowX: 'auto', width: '100%' }}>
|
||
<div className='d-flex' style={{ gap: '1rem' }}>
|
||
<div className='audioInputs'>
|
||
<h5>Audio Inputs <FontAwesomeIcon icon="question-circle" id="audioInputsTooltip" className="ml-2" style={{ cursor: 'pointer' }} /></h5>
|
||
<div style={{ borderRight: '1px #ddd solid', paddingRight: '1rem' }}>
|
||
<JKSessionAudioInputs
|
||
myTracks={mixerHelper.myTracks}
|
||
chat={chat}
|
||
mixerHelper={mixerHelper}
|
||
isRemote={false}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className='sessionMix'>
|
||
<h5>Session Mix <FontAwesomeIcon icon="question-circle" id="sessionMixTooltip" className="ml-2" style={{ cursor: 'pointer' }} /></h5>
|
||
<div className='d-flex' style={{ gap: '1rem' }}>
|
||
<JKSessionAudioInputs
|
||
myTracks={mixerHelper.myTracks}
|
||
chat={chat}
|
||
mixerHelper={mixerHelper}
|
||
isRemote={true}
|
||
/>
|
||
<JKSessionRemoteTracks
|
||
mixerHelper={mixerHelper}
|
||
sessionModel={sessionModel}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* JamTrack Section - Show stems when player is ready */}
|
||
{selectedJamTrack && jamTrackStems.length > 0 &&
|
||
(jamTrackDownloadState.state === 'synchronized' || jamTrackDownloadState.state === 'idle') && (
|
||
<>
|
||
<div style={{ borderLeft: '1px solid #ddd', paddingLeft: '1rem' }}></div>
|
||
<div className='jamTrack'>
|
||
<h5>
|
||
JamTrack: {selectedJamTrack.name}
|
||
<a
|
||
href="#"
|
||
className="text-muted ml-2"
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
handleJamTrackClose();
|
||
}}
|
||
style={{ fontSize: '1.2em', textDecoration: 'none' }}
|
||
title="Close JamTrack"
|
||
>
|
||
<FontAwesomeIcon icon="times" /> Close
|
||
</a>
|
||
</h5>
|
||
<JKSessionJamTrackStems
|
||
jamTrackStems={jamTrackStems}
|
||
mixerHelper={mixerHelper}
|
||
/>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* Backing Track Section - Show track when player is open */}
|
||
{showBackingTrackPlayer && mixerHelper.backingTracks && mixerHelper.backingTracks.length > 0 && (
|
||
<>
|
||
<div style={{ borderLeft: '1px solid #ddd', paddingLeft: '1rem' }}></div>
|
||
<div className='backingTrack'>
|
||
<h5>
|
||
Backing Track: {mixerHelper.backingTracks[0].shortFilename || 'Audio File'}
|
||
<a
|
||
href="#"
|
||
className="text-muted ml-2"
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
handleBackingTrackMainClose();
|
||
}}
|
||
style={{ fontSize: '1.2em', textDecoration: 'none' }}
|
||
title="Close Backing Track"
|
||
>
|
||
<FontAwesomeIcon icon="times" /> Close
|
||
</a>
|
||
</h5>
|
||
<JKSessionBackingTrack
|
||
backingTrack={mixerHelper.backingTracks[0]}
|
||
mixers={mixerHelper.backingTracks[0].mixers}
|
||
onClose={handleBackingTrackMainClose}
|
||
/>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* Metronome Section - Show track when metronome is open */}
|
||
{metronomeState.isOpen && metronomeTrackMixers && metronomeTrackMixers.length > 0 && (
|
||
<>
|
||
<div style={{ borderLeft: '1px solid #ddd', paddingLeft: '1rem' }}></div>
|
||
<div className='metronomeTrack'>
|
||
<h5>
|
||
Metronome
|
||
<a
|
||
href="#"
|
||
className="text-muted ml-2"
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
handleMetronomeClose();
|
||
}}
|
||
style={{ fontSize: '1.2em', textDecoration: 'none' }}
|
||
title="Close Metronome"
|
||
>
|
||
<FontAwesomeIcon icon="times" /> Close
|
||
</a>
|
||
</h5>
|
||
<JKSessionMetronome
|
||
mixers={metronomeTrackMixers[0]}
|
||
onClose={handleMetronomeClose}
|
||
/>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
</div>
|
||
|
||
{/* Connection Status Alerts */}
|
||
{showConnectionAlert && (
|
||
<div className='mt-4'>
|
||
<Alert color={
|
||
connectionStatus === ConnectionStatus.DISCONNECTED ? 'warning' :
|
||
connectionStatus === ConnectionStatus.RECONNECTING ? 'info' :
|
||
connectionStatus === ConnectionStatus.ERROR ? 'danger' : 'success'
|
||
}>
|
||
<div className='d-flex align-items-center'>
|
||
<div className='me-2'>
|
||
{connectionStatus === ConnectionStatus.DISCONNECTED && '⚠️'}
|
||
{connectionStatus === ConnectionStatus.RECONNECTING && '🔄'}
|
||
{connectionStatus === ConnectionStatus.ERROR && '❌'}
|
||
{connectionStatus === ConnectionStatus.CONNECTED && '✅'}
|
||
</div>
|
||
<div>
|
||
{connectionStatus === ConnectionStatus.DISCONNECTED && (
|
||
<strong>Connection Lost</strong>
|
||
)}
|
||
{connectionStatus === ConnectionStatus.RECONNECTING && (
|
||
<div>
|
||
<strong>Reconnecting...</strong>
|
||
{reconnectAttempts > 0 && (
|
||
<div className='small mt-1'>
|
||
Attempt {reconnectAttempts} of 10
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
{connectionStatus === ConnectionStatus.ERROR && (
|
||
<div>
|
||
<strong>Connection Failed</strong>
|
||
{lastError && (
|
||
<div className='small mt-1'>
|
||
{lastError.message || 'Unknown error'}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
{connectionStatus === ConnectionStatus.CONNECTED && (
|
||
<strong>Reconnected Successfully</strong>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</Alert>
|
||
</div>
|
||
)}
|
||
|
||
{/* Connection Status Badge in Header */}
|
||
<div className='mt-3 mb-3'>
|
||
<Badge
|
||
color={
|
||
connectionStatus === ConnectionStatus.CONNECTED ? 'success' :
|
||
connectionStatus === ConnectionStatus.CONNECTING ? 'warning' :
|
||
connectionStatus === ConnectionStatus.RECONNECTING ? 'info' :
|
||
connectionStatus === ConnectionStatus.ERROR ? 'danger' : 'secondary'
|
||
}
|
||
className='me-2'
|
||
>
|
||
{connectionStatus === ConnectionStatus.CONNECTED && '🟢 Connected'}
|
||
{connectionStatus === ConnectionStatus.CONNECTING && '🟡 Connecting...'}
|
||
{connectionStatus === ConnectionStatus.RECONNECTING && '🔄 Reconnecting...'}
|
||
{connectionStatus === ConnectionStatus.DISCONNECTED && '🟠 Disconnected'}
|
||
{connectionStatus === ConnectionStatus.ERROR && '🔴 Error'}
|
||
</Badge>
|
||
{reconnectAttempts > 0 && (
|
||
<Badge color='info' className='me-2'>
|
||
Attempt {reconnectAttempts}/10
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
|
||
{/* Debug Info */}
|
||
{/* <div className='mt-4 p-3 bg-white rounded'>
|
||
<h6>Debug Information</h6>
|
||
<div className='row'>
|
||
<div className='col-md-3'>
|
||
<strong>Connection:</strong> {isConnected ? '✅ Connected' : '❌ Disconnected'}
|
||
</div>
|
||
<div className='col-md-3'>
|
||
<strong>Status:</strong> {connectionStatus}
|
||
</div>
|
||
</div>
|
||
<div className='row mt-2'>
|
||
<div className='col-md-3'>
|
||
<strong>Reconnect Attempts:</strong> {reconnectAttempts}
|
||
</div>
|
||
<div className='col-md-6'>
|
||
<strong>Last Error:</strong> {lastError ? lastError.message : 'None'}
|
||
</div>
|
||
</div>
|
||
<div className='row mt-2'>
|
||
<div className='col-md-12'>
|
||
<strong>Current Session:</strong> {JSON.stringify(currentSession)}
|
||
</div>
|
||
</div>
|
||
</div> */}
|
||
</CardBody>
|
||
|
||
<JKSessionSettingsModal
|
||
isOpen={showSettingsModal}
|
||
toggle={() => dispatch(toggleModal('settings'))}
|
||
currentSession={{ ...currentSession, privacy: musicianAccess }}
|
||
loading={settingsLoading}
|
||
onSave={async (payload) => {
|
||
console.log('Session settings :', payload);
|
||
try {
|
||
setSettingsLoading(true);
|
||
|
||
switch (parseInt(payload.privacy)) {
|
||
case SESSION_PRIVACY_MAP['public']:
|
||
payload.musician_access = true;
|
||
payload.approval_required = false;
|
||
break;
|
||
case SESSION_PRIVACY_MAP['private_approve']:
|
||
payload.musician_access = true;
|
||
payload.approval_required = true;
|
||
break;
|
||
case SESSION_PRIVACY_MAP['private_invite']:
|
||
payload.musician_access = false;
|
||
payload.approval_required = false;
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
const response = await updateSessionSettings({
|
||
id: currentSessionIdRef.current,
|
||
...payload
|
||
});
|
||
const data = await response.json();
|
||
console.log('Updated session settings response:', data);
|
||
dispatch(updateSessionData(data)); // Phase 4: dispatch to Redux
|
||
dispatch(closeModal('settings'));
|
||
toast.success('Session settings updated successfully');
|
||
} catch (error) {
|
||
console.error('Error updating session settings:', error);
|
||
toast.error('Failed to update session settings');
|
||
} finally {
|
||
setSettingsLoading(false);
|
||
}
|
||
|
||
}}
|
||
/>
|
||
|
||
<JKSessionInviteModal
|
||
currentSession={currentSession}
|
||
show={showInviteModal}
|
||
size="lg"
|
||
onToggle={() => dispatch(closeModal('invite'))}
|
||
friends={friends}
|
||
initialInvitees={sessionInvitees}
|
||
loading={inviteLoading}
|
||
onSubmit={async (invitees) => {
|
||
setSessionInvitees(invitees);
|
||
console.log('Submitted invitees:', invitees);
|
||
const inviteeIds = invitees.map(i => i.id)
|
||
const payload = {
|
||
inviteeIds: inviteeIds.join()
|
||
};
|
||
try {
|
||
setInviteLoading(true);
|
||
const response = await updateSessionSettings({
|
||
id: currentSessionIdRef.current,
|
||
...payload
|
||
});
|
||
const data = await response.json();
|
||
console.log('Updated session settings response:', data);
|
||
dispatch(updateSessionData(data)); // Phase 4: dispatch to Redux
|
||
dispatch(closeModal('invite'));
|
||
toast.success('Invitations sent successfully');
|
||
} catch (error) {
|
||
console.error('Error updating session settings:', error);
|
||
toast.error('Failed to send invitations');
|
||
} finally {
|
||
setInviteLoading(false);
|
||
}
|
||
}}
|
||
/>
|
||
|
||
<JKSessionVolumeModal
|
||
isOpen={showVolumeModal}
|
||
toggle={() => dispatch(toggleModal('volume'))}
|
||
/>
|
||
|
||
<JKSessionRecordingModal
|
||
isOpen={showRecordingModal}
|
||
toggle={() => dispatch(toggleModal('recording'))}
|
||
onSubmit={handleRecordingSubmit}
|
||
/>
|
||
|
||
<JKSessionLeaveModal
|
||
isOpen={showLeaveModal}
|
||
toggle={() => dispatch(closeModal('leave'))} // Just close modal, don't navigate since session not left yet
|
||
onSubmit={handleLeaveSubmit}
|
||
loading={leaveLoading}
|
||
/>
|
||
|
||
<UncontrolledTooltip target="audioInputsTooltip" trigger="hover click">
|
||
<div>
|
||
<p>Set the input level of your Audio Inputs for each of your tracks to a healthy level. It's important to set your input level correctly. If your level is set too high, you'll get distortion or clipping of your audio. If set too low, your audio signal will be too weak, which can cause noise and degrade your audio quality when you and others use the session mix to increase your volume in the mix.</p>
|
||
<p>For instructions on how to set your Audio Input levels, read <a href="/help/audio-input-levels" target="_blank">this help article</a>.</p>
|
||
</div>
|
||
</UncontrolledTooltip>
|
||
|
||
<UncontrolledTooltip target="sessionMixTooltip" trigger="hover click">
|
||
<div>
|
||
<p>Adjust the volume of each audio track (yours and others) in the Session Mix to get the mix where you want it (i.e. where it sounds good and well balanced to you). Any volume changes you make will affect only what you hear. They don’t affect the volume of what others hear in the session. Everyone has their own customizable session mix.</p>
|
||
<p>Note that your session mix is the mix that will be used for any recordings you make and for any broadcasts you stream. If another musician in your session makes a recording or streams a broadcast, it will use that musician’s session mix, not yours.</p>
|
||
<p>For instructions on how to set your Session Mix levels, <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000124834" target="_blank" rel="noopener noreferrer">read this help article</a>.</p>
|
||
</div>
|
||
</UncontrolledTooltip>
|
||
|
||
{/* Backing Track Popup */}
|
||
{showBackingTrackPopup && backingTrackData && (
|
||
<WindowPortal
|
||
onClose={handleBackingTrackClose}
|
||
windowFeatures="width=500,height=400,left=200,top=200,menubar=no,toolbar=no,status=no,scrollbars=yes,resizable=yes,location=no, addressbar=no"
|
||
>
|
||
<JKSessionBackingTrackPlayer
|
||
backingTrack={backingTrackData.backingTrack}
|
||
jamClient={jamClient}
|
||
session={backingTrackData.session}
|
||
currentUser={backingTrackData.currentUser}
|
||
isPopup={true}
|
||
onClose={() => {
|
||
console.log('JKSessionScreen: JKSessionBackingTrackPlayer onClose called');
|
||
dispatch(closeModal('backingTrack'));
|
||
dispatch(clearBackingTrackData());
|
||
}}
|
||
/>
|
||
</WindowPortal>
|
||
)}
|
||
|
||
{/* Metronome Player Popup */}
|
||
{metronomeState.isOpen && (
|
||
<WindowPortal
|
||
title="Metronome Controls"
|
||
onClose={handleMetronomeClose}
|
||
windowFeatures="width=450,height=400,left=200,top=200,menubar=no,toolbar=no,status=no,scrollbars=yes,resizable=yes,location=no, addressbar=no"
|
||
windowId="metronome-controls"
|
||
>
|
||
<JKSessionMetronomePlayer
|
||
isOpen={metronomeState.isOpen}
|
||
onClose={handleMetronomeClose}
|
||
metronomeState={metronomeState}
|
||
jamClient={jamClient}
|
||
session={currentSession}
|
||
currentUser={currentUser}
|
||
isPopup={true}
|
||
/>
|
||
</WindowPortal>
|
||
)}
|
||
|
||
{/* JamTrack Player Popup */}
|
||
{showJamTrackPlayer && jamTrackData && (
|
||
<WindowPortal
|
||
onClose={handleJamTrackPlayerClose}
|
||
windowFeatures="width=600,height=500,left=200,top=200,menubar=no,toolbar=no,status=no,scrollbars=yes,resizable=yes,location=no, addressbar=no"
|
||
>
|
||
<JKSessionJamTrackPlayer
|
||
jamTrack={jamTrackData.jamTrack}
|
||
jamClient={jamClient}
|
||
session={jamTrackData.session}
|
||
currentUser={jamTrackData.currentUser}
|
||
isPopup={true}
|
||
onClose={handleJamTrackClose}
|
||
/>
|
||
</WindowPortal>
|
||
)}
|
||
|
||
<JKSessionJamTrackModal
|
||
isOpen={showJamTrackModal}
|
||
toggle={() => dispatch(toggleModal('jamTrack'))}
|
||
onJamTrackSelect={handleJamTrackSelect}
|
||
/>
|
||
|
||
{/* Chat Window */}
|
||
<JKSessionChatWindow />
|
||
|
||
{/* Media Controls Popup - Only show when explicitly opened */}
|
||
{showMediaControlsPopup && !popupGuard && (() => {
|
||
console.log('JKSessionScreen: RENDERING Media Controls Popup - showMediaControlsPopup:', showMediaControlsPopup, 'popupGuard:', popupGuard);
|
||
setPopupGuard(true); // Set guard immediately to prevent re-renders
|
||
return (
|
||
<WindowPortal
|
||
onClose={() => {
|
||
console.log('JKSessionScreen: Media Controls Popup closing');
|
||
dispatch(closeModal('mediaControls'));
|
||
setMediaControlsOpened(false);
|
||
setPopupGuard(false); // Reset guard when closing
|
||
}}
|
||
windowFeatures="width=600,height=500,left=250,top=150,menubar=no,toolbar=no,status=no,scrollbars=yes,resizable=yes,location=no,addressbar=no"
|
||
title="Media Controls"
|
||
windowId="media-controls"
|
||
>
|
||
<JKPopupMediaControls onClose={() => {
|
||
console.log('JKSessionScreen: JKPopupMediaControls onClose called');
|
||
dispatch(closeModal('mediaControls'));
|
||
setMediaControlsOpened(false);
|
||
setPopupGuard(false); // Reset guard when closing
|
||
}} />
|
||
</WindowPortal>
|
||
);
|
||
})()}
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
export default memo(JKSessionScreen)
|