"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { Box, Button, HStack, Icon, Spinner, Text, VStack } from "@chakra-ui/react"; import { useRouter } from "next/navigation"; import { useRoomGetByName, useRoomJoinMeeting, useMeetingAudioConsent } from "../../lib/apiHooks"; import { useRecordingConsent } from "../../recordingConsentContext"; import { toaster } from "../../components/ui/toaster"; import { FaBars } from "react-icons/fa6"; import MinimalHeader from "../../components/MinimalHeader"; import type { components } from "../../reflector-api"; type Meeting = components["schemas"]["Meeting"]; // next throws even with "use client" const useWhereby = () => { const [wherebyLoaded, setWherebyLoaded] = useState(false); useEffect(() => { if (typeof window !== "undefined") { import("@whereby.com/browser-sdk/embed") .then(() => { setWherebyLoaded(true); }) .catch(console.error.bind(console)); } }, []); return wherebyLoaded; }; // Consent functionality from main branch const useConsentWherebyFocusManagement = ( acceptButtonRef: React.RefObject, wherebyRef: React.RefObject, ) => { const currentFocusRef = useRef(null); useEffect(() => { if (acceptButtonRef.current) { acceptButtonRef.current.focus(); } else { console.error( "accept button ref not available yet for focus management - seems to be illegal state", ); } const handleWherebyReady = () => { console.log("whereby ready - refocusing consent button"); currentFocusRef.current = document.activeElement as HTMLElement; if (acceptButtonRef.current) { acceptButtonRef.current.focus(); } }; if (wherebyRef.current) { wherebyRef.current.addEventListener("ready", handleWherebyReady); } else { console.warn( "whereby ref not available yet for focus management - seems to be illegal state. not waiting, focus management off.", ); } return () => { wherebyRef.current?.removeEventListener("ready", handleWherebyReady); currentFocusRef.current?.focus(); }; }, []); }; const useConsentDialog = ( meetingId: string, wherebyRef: React.RefObject, ) => { const { state: consentState, touch, hasConsent } = useRecordingConsent(); const [modalOpen, setModalOpen] = useState(false); const audioConsentMutation = useMeetingAudioConsent(); const handleConsent = useCallback( async (meetingId: string, given: boolean) => { try { await audioConsentMutation.mutateAsync({ params: { path: { meeting_id: meetingId, }, }, body: { consent_given: given, }, }); touch(meetingId); } catch (error) { console.error("Error submitting consent:", error); } }, [audioConsentMutation, touch], ); const showConsentModal = useCallback(() => { if (modalOpen) return; setModalOpen(true); const toastId = toaster.create({ placement: "top", duration: null, render: ({ dismiss }) => { const AcceptButton = () => { const buttonRef = useRef(null); useConsentWherebyFocusManagement(buttonRef, wherebyRef); return ( ); }; return ( Can we have your permission to store this meeting's audio recording on our servers? ); }, }); // Set modal state when toast is dismissed toastId.then((id) => { const checkToastStatus = setInterval(() => { if (!toaster.isActive(id)) { setModalOpen(false); clearInterval(checkToastStatus); } }, 100); }); // Handle escape key to close the toast const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { toastId.then((id) => toaster.dismiss(id)); } }; document.addEventListener("keydown", handleKeyDown); const cleanup = () => { toastId.then((id) => toaster.dismiss(id)); document.removeEventListener("keydown", handleKeyDown); }; return cleanup; }, [meetingId, handleConsent, wherebyRef, modalOpen]); return { showConsentModal, consentState, hasConsent, consentLoading: audioConsentMutation.isPending, }; }; function ConsentDialogButton({ meetingId, wherebyRef, }: { meetingId: string; wherebyRef: React.RefObject; }) { const { showConsentModal, consentState, hasConsent, consentLoading } = useConsentDialog(meetingId, wherebyRef); if (!consentState.ready || hasConsent(meetingId) || consentLoading) { return null; } return ( ); } const recordingTypeRequiresConsent = ( recordingType: NonNullable, ) => { return recordingType === "cloud"; }; interface MeetingPageProps { params: { roomName: string; meetingId: string; }; } export default function MeetingPage({ params }: MeetingPageProps) { const { roomName, meetingId } = params; const router = useRouter(); const [attemptedJoin, setAttemptedJoin] = useState(false); const wherebyLoaded = useWhereby(); const wherebyRef = useRef(null); // Fetch room data const roomQuery = useRoomGetByName(roomName); const joinMeetingMutation = useRoomJoinMeeting(); const room = roomQuery.data; const isLoading = roomQuery.isLoading || (!attemptedJoin && room && !joinMeetingMutation.data); // Try to join the meeting when room is loaded useEffect(() => { if (room && !attemptedJoin && !joinMeetingMutation.isPending) { setAttemptedJoin(true); joinMeetingMutation.mutate({ params: { path: { room_name: roomName, meeting_id: meetingId, }, }, }); } }, [room, attemptedJoin, joinMeetingMutation, roomName, meetingId]); // Redirect to room lobby if meeting join fails (meeting finished/not found) useEffect(() => { if (joinMeetingMutation.isError || roomQuery.isError) { router.push(`/${roomName}`); } }, [joinMeetingMutation.isError, roomQuery.isError, router, roomName]); // Get meeting data from join response const meeting = joinMeetingMutation.data; const roomUrl = meeting?.host_room_url || meeting?.room_url; const recordingType = meeting?.recording_type; const handleLeave = useCallback(() => { router.push(`/${roomName}`); }, [router, roomName]); useEffect(() => { if (!isLoading && !roomUrl && !wherebyLoaded) return; wherebyRef.current?.addEventListener("leave", handleLeave); return () => { wherebyRef.current?.removeEventListener("leave", handleLeave); }; }, [handleLeave, roomUrl, isLoading, wherebyLoaded]); if (isLoading) { return ( Loading meeting... ); } // If we have a successful meeting join with room URL, show Whereby embed if (meeting && roomUrl && wherebyLoaded) { return ( <> {recordingType && recordingTypeRequiresConsent(recordingType) && ( )} ); } // This return should not be reached normally since we redirect on errors // But keeping it as a fallback return ( Meeting not available ); }