From 808aa7fac76ba7bd52c8dfddbc95ec3adbf29f7f Mon Sep 17 00:00:00 2001 From: "mateusz.bieniek" Date: Thu, 30 Apr 2026 13:10:45 +0200 Subject: [PATCH 1/2] Jitsy update, add webinar links on mobile --- .../ConsultationCard/JitsyMeeting/index.tsx | 38 +++++++++--- .../MeetingAnalyticsOverlay.tsx | 14 ++++- .../Webinar/WebinarMeetModal/index.tsx | 9 ++- src/components/_App/Navbar/index.tsx | 21 ++++++- src/hooks/meeting/useCamera.ts | 60 ++++++++++++------- src/hooks/useAnalyticsWebsockets.ts | 5 +- 6 files changed, 108 insertions(+), 39 deletions(-) diff --git a/src/components/Consultations/ConsultationCard/JitsyMeeting/index.tsx b/src/components/Consultations/ConsultationCard/JitsyMeeting/index.tsx index af2bcfa2..2486b526 100644 --- a/src/components/Consultations/ConsultationCard/JitsyMeeting/index.tsx +++ b/src/components/Consultations/ConsultationCard/JitsyMeeting/index.tsx @@ -12,10 +12,7 @@ import { API_URL } from "@/config/index"; import useCamera, { cameraPermissions } from "@/hooks/meeting/useCamera"; import { API } from "@escolalms/sdk/lib"; import { IMeetRecording } from "@/components/Consultations/ConsultationCard/JitsyMeeting/types"; -import { - JITSY_ANALYTICS_INTERVAL, - JITSY_TUTOR_INTERVAL, -} from "@/utils/constants"; +import { JITSY_ANALYTICS_INTERVAL } from "@/utils/constants"; import { Text } from "@escolalms/components/lib/components/atoms/Typography/Text"; import { Button } from "@escolalms/components/lib/components/atoms/Button/Button"; import { useJitsyAnalyticsControl } from "@/hooks/meeting/useJitsyAnalyticsControl"; @@ -78,7 +75,7 @@ const JitsyMeeting: React.FC = ({ const { consultation, token } = useContext(EscolaLMSContext); const { isStudent } = useRoles(); const { t } = useTranslation(); - const { camera, getDataUrl, cameraAccessStatus } = useCamera(); + const { camera, getDataUrl, cameraAccessStatus, stopCamera } = useCamera(); const userConsentedRef = useRef(false); const isCameraMutedRef = useRef(false); @@ -91,6 +88,8 @@ const JitsyMeeting: React.FC = ({ const workerRef = useRef(null); const apiRef = useRef(null); const recordingTimeoutRef = useRef(null); + const hasSentEndEventRef = useRef(false); + const { shouldRunAnalytics } = useJitsyAnalyticsControl({ modelType, participantCount, @@ -140,6 +139,13 @@ const JitsyMeeting: React.FC = ({ const sendRecordingEvent = useCallback( async (action: "start-recording" | "end-recording") => { if (isStudent) return; + + if ( + action === "end-recording" && + (hasSentEndEventRef.current || !recordingIdRef.current) + ) { + return; + } try { const payload = preparePayload(action); @@ -156,6 +162,10 @@ const JitsyMeeting: React.FC = ({ } ); + if (action === "end-recording") { + recordingIdRef.current = null; + } + const result = await response.json(); if (action === "start-recording" && result?.data?.id) { @@ -364,13 +374,25 @@ const JitsyMeeting: React.FC = ({ useEffect(() => { const init = async () => { try { - await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); + const stream = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: true, + }); + + stream.getTracks().forEach((track) => { + track.stop(); + console.log("Init test track stopped"); + }); + isStudent ? setShowModal(true) : setShowMeeting(true); - } catch { + } catch (err) { + console.warn("Camera access failed in init", err); setShowMeeting(true); } }; init(); + + return () => {}; }, [isStudent]); const getProperRoomName = () => { @@ -392,7 +414,9 @@ const JitsyMeeting: React.FC = ({ iframeRef.style.width = "100%"; }} onReadyToClose={async () => { + console.log("Closing meeting..."); stopAllIntervals(); + stopCamera(); if (!isStudent && recordingIdRef.current) { await sendRecordingEvent("end-recording"); } diff --git a/src/components/MeetingAnalyticsOverlay/MeetingAnalyticsOverlay.tsx b/src/components/MeetingAnalyticsOverlay/MeetingAnalyticsOverlay.tsx index 856b44e6..77abc6a6 100644 --- a/src/components/MeetingAnalyticsOverlay/MeetingAnalyticsOverlay.tsx +++ b/src/components/MeetingAnalyticsOverlay/MeetingAnalyticsOverlay.tsx @@ -213,12 +213,16 @@ export default function MeetingAnalyticsOverlay({ isArea: boolean ) => { if (data.length === 0) return null; + const isSinglePoint = data.length === 1; + return ( {data.map((point, index) => ( @@ -233,6 +237,14 @@ export default function MeetingAnalyticsOverlay({ val: 0, }; + useEffect(() => { + if (!shouldRunAnalytics) { + setAttentionData([]); + setEmotionHistory([]); + setShouldBreak(false); + } + }, [shouldRunAnalytics]); + return (
diff --git a/src/components/Webinars/Webinar/WebinarMeetModal/index.tsx b/src/components/Webinars/Webinar/WebinarMeetModal/index.tsx index 929c6b42..e49e83b3 100644 --- a/src/components/Webinars/Webinar/WebinarMeetModal/index.tsx +++ b/src/components/Webinars/Webinar/WebinarMeetModal/index.tsx @@ -12,7 +12,6 @@ import MeetingAnalyticsOverlay from "@/components/MeetingAnalyticsOverlay/Meetin import { EndMeetingQuestionnairesModal } from "@/components/Consultations/ConsultationCard/EndMeetingQuestionnaires"; import { QuestionnaireModelType } from "@/types/questionnaire"; import { API } from "@escolalms/sdk/lib"; -import { useRoles } from "@/hooks/useRoles"; interface Props { onClose: () => void; @@ -39,7 +38,6 @@ const WebinarMeetModal = ({ onClose, visible, webinarId, webinar }: Props) => { const onCloseRef = useRef(onClose); const { generateWebinarJitsy } = useContext(EscolaLMSContext); const { t } = useTranslation(); - const { isTutor } = useRoles(); useEffect(() => { onCloseRef.current = onClose; @@ -86,8 +84,9 @@ const WebinarMeetModal = ({ onClose, visible, webinarId, webinar }: Props) => { const handleOnClose = useCallback(() => { setIsEnded(true); - isTutor && onClose(); - }, [isTutor, onClose]); + setWebinarMeetData(null); + onClose(); + }, [onClose]); return ( <> @@ -114,7 +113,7 @@ const WebinarMeetModal = ({ onClose, visible, webinarId, webinar }: Props) => { webinar={webinar} participantCount={participantCount} /> - {!loading && webinarMeetData && ( + {visible && !loading && webinarMeetData && ( { ), key: "menu-2", }, + { + title: ( + + + {t("Menu.Webinars")} + + + ), + key: "menu-3", + }, { title: ( @@ -352,7 +362,7 @@ const Navbar = () => { ), - key: "menu-3", + key: "menu-4", }, { title: ( @@ -362,7 +372,7 @@ const Navbar = () => { ), - key: "menu-4", + key: "menu-5", }, { title: settings?.value?.config?.[metaDataKeys.termsPageMetaKey] && ( @@ -374,7 +384,7 @@ const Navbar = () => { ), - key: "menu-5", + key: "menu-6", }, { @@ -504,6 +514,11 @@ const Navbar = () => { {t("MyProfilePage.MyConsultations")} +
  • + + {t("MyProfilePage.MyWebinars")} + +
  • {t("Navbar.MyCertificates")} diff --git a/src/hooks/meeting/useCamera.ts b/src/hooks/meeting/useCamera.ts index 2b35e372..3d559f4f 100644 --- a/src/hooks/meeting/useCamera.ts +++ b/src/hooks/meeting/useCamera.ts @@ -130,40 +130,55 @@ const useCamera = () => { useEffect(() => { const handlePermissionChange = async () => { - const permissionStatus = await navigator.permissions.query({ - name: "camera" as PermissionName, - }); + try { + const permissionStatus = await navigator.permissions.query({ + name: "camera" as PermissionName, + }); - // If permission is granted, restart the video track - if (permissionStatus.state === cameraPermissions.GRANTED) { - await restartVideoTrack(); - } + const updateStatus = () => { + if (permissionStatus.state === cameraPermissions.DENIED) { + setHasCameraAccess(false); + setCameraAccessStatus(cameraPermissions.DENIED); + } else { + setCameraAccessStatus(permissionStatus.state as cameraPermissions); + } + }; - if (permissionStatus.state === cameraPermissions.DENIED) { - setHasCameraAccess(false); - setCameraAccessStatus(cameraPermissions.DENIED); + permissionStatus.onchange = updateStatus; + updateStatus(); + } catch (e) { + console.error("Permissions API not supported", e); } - - // Listen for permission changes - permissionStatus.onchange = async () => { - if (permissionStatus.state === cameraPermissions.GRANTED) { - await restartVideoTrack(); - } else { - setHasCameraAccess(false); - setCameraAccessStatus(cameraPermissions.DENIED); - } - }; }; handlePermissionChange(); return () => { - // Clean up Workera if (workerRef.current) { workerRef.current.terminate(); } + + if (streamRef.current) { + streamRef.current.getTracks().forEach((t) => t.stop()); + } }; - }, [restartVideoTrack]); + }, []); + + const stopCamera = useCallback(() => { + if (streamRef.current) { + streamRef.current.getTracks().forEach((track) => { + track.stop(); + }); + streamRef.current = null; + } + + if (videoRef.current) { + videoRef.current.srcObject = null; + videoRef.current.pause(); + } + + setHasCameraAccess(false); + }, []); return { camera, @@ -171,6 +186,7 @@ const useCamera = () => { restartVideoTrack, getDataUrl, cameraAccessStatus, + stopCamera, }; }; diff --git a/src/hooks/useAnalyticsWebsockets.ts b/src/hooks/useAnalyticsWebsockets.ts index 490ca5d8..d304bc7b 100644 --- a/src/hooks/useAnalyticsWebsockets.ts +++ b/src/hooks/useAnalyticsWebsockets.ts @@ -11,7 +11,10 @@ export const useMeetingSockets = ( const [socketData, setSocketData] = useState(null); useEffect(() => { - if (!modelId || !termUnix || !token) return; + if (!modelId || !termUnix || !token) { + setSocketData(null); + return; + } const echo = getEchoInstance(token); if (!echo) return; From e30aebef9dab151186b6afdd7628283803d2e59c Mon Sep 17 00:00:00 2001 From: "mateusz.bieniek" Date: Thu, 30 Apr 2026 13:18:07 +0200 Subject: [PATCH 2/2] Clog cleanup --- .../Consultations/ConsultationCard/JitsyMeeting/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Consultations/ConsultationCard/JitsyMeeting/index.tsx b/src/components/Consultations/ConsultationCard/JitsyMeeting/index.tsx index 2486b526..1d5b4af6 100644 --- a/src/components/Consultations/ConsultationCard/JitsyMeeting/index.tsx +++ b/src/components/Consultations/ConsultationCard/JitsyMeeting/index.tsx @@ -414,7 +414,6 @@ const JitsyMeeting: React.FC = ({ iframeRef.style.width = "100%"; }} onReadyToClose={async () => { - console.log("Closing meeting..."); stopAllIntervals(); stopCamera(); if (!isStudent && recordingIdRef.current) {