import React, { useState, useEffect } from 'react' import { MdMoreVert, MdVideocam, MdPersonAdd, MdEdit, MdOpenInNew, MdDelete } from 'react-icons/md' import { Dropdown } from './Dropdown' import { FaYoutube, FaFacebook, FaTwitch, FaLinkedin } from 'react-icons/fa' import { SkeletonTable } from './Skeleton' import styles from './TransmissionsTable.module.css' import InviteGuestsModal from './InviteGuestsModal' import { NewTransmissionModal } from '@shared/components' import type { Transmission } from '@shared/types' import useStudioLauncher from '../hooks/useStudioLauncher' import useStudioMessageListener from '../hooks/useStudioMessageListener' import StudioPortal from '../features/studio/StudioPortal' interface Props { transmissions: Transmission[] onDelete: (id: string) => void onUpdate: (t: Transmission) => void isLoading?: boolean } const platformIcons: Record = { 'YouTube': , 'Facebook': , 'Twitch': , 'LinkedIn': , 'Generico': , // Logo genérico para transmisiones sin destino } const TransmissionsTable: React.FC = (props) => { const { transmissions, onDelete, onUpdate, isLoading } = props const [activeTab, setActiveTab] = useState<'upcoming' | 'past'>('upcoming') const [inviteOpen, setInviteOpen] = useState(false) const [inviteLink, setInviteLink] = useState(undefined) const [editOpen, setEditOpen] = useState(false) const [editTransmission, setEditTransmission] = useState(undefined) const { openStudio, loadingId: launcherLoadingId, error: launcherError } = useStudioLauncher() const [loadingId, setLoadingId] = useState(null) const [studioSession, setStudioSession] = useState<{ serverUrl?: string; token?: string; room?: string } | null>(null) const [validating, setValidating] = useState(false) const [connectError, setConnectError] = useState(null) const [currentAttempt, setCurrentAttempt] = useState(null) // Listen for external postMessage events carrying a LIVEKIT_TOKEN payload. useStudioMessageListener((msg) => { try { if (msg && msg.token) { const serverUrl = msg.url || (import.meta.env.VITE_LIVEKIT_WS_URL as string) || '' // start validating token and open StudioPortal overlay setValidating(true) setConnectError(null) setStudioSession({ serverUrl, token: msg.token, room: msg.room || 'external' }) } } catch (e) { /* ignore */ } }) // Auto-open studio if token is present in URL (INCLUDE_TOKEN_IN_REDIRECT flow) useEffect(() => { try { if (typeof window === 'undefined') return const params = new URLSearchParams(window.location.search) const tokenParam = params.get('token') if (tokenParam) { const serverParam = params.get('serverUrl') || params.get('url') || (import.meta.env.VITE_LIVEKIT_WS_URL as string) || '' const roomParam = params.get('room') || 'external' setConnectError(null) setValidating(true) setStudioSession({ serverUrl: serverParam, token: tokenParam, room: roomParam }) } } catch (e) { /* ignore */ } }, []) const handleEdit = (t: Transmission) => { setEditTransmission(t) setEditOpen(true) } // Filtrado por fechas const filtered = transmissions.filter((t: Transmission) => { // Si es "Próximamente" o no tiene fecha programada, siempre va a "upcoming" if (!t.scheduled || t.scheduled === 'Próximamente') return activeTab === 'upcoming' const scheduledDate = new Date(t.scheduled) const now = new Date() if (activeTab === 'upcoming') { return scheduledDate >= now } else { return scheduledDate < now } }) const openStudioForTransmission = async (t: Transmission) => { if (loadingId || launcherLoadingId) return setLoadingId(t.id) setCurrentAttempt(t) setValidating(true) try { const userRaw = localStorage.getItem('avanzacast_user') || 'Demo User' const user = (userRaw) const room = (t.id || 'avanzacast-studio') const result = await openStudio({ room, username: user }) if (!result) { throw new Error('No se pudo abrir el estudio') } const resAny: any = result as any // If backend returned a session id, persist it and navigate to broadcastPanel/:id so the Studio route picks it if (resAny && resAny.id) { try { const storeKey = (import.meta.env.VITE_STUDIO_SESSION_KEY as string) || 'avanzacast_studio_session' sessionStorage.setItem(storeKey, JSON.stringify(resAny)) try { window.dispatchEvent(new CustomEvent('AVZ_STUDIO_SESSION', { detail: resAny })) } catch (e) { /* ignore */ } } catch (e) { /* ignore storage errors */ } const BROADCAST_BASE = (import.meta.env.VITE_BROADCASTPANEL_URL as string) || (typeof window !== 'undefined' ? window.location.origin : '') const target = `${BROADCAST_BASE.replace(/\/$/, '')}/${encodeURIComponent(resAny.id)}` try { window.location.href = target return } catch (e) { try { window.location.assign(target) } catch (e2) { /* ignore */ } } } // If app is configured as integrated, ensure we open StudioPortal overlay immediately const INTEGRATED = (import.meta.env.VITE_STUDIO_INTEGRATED === 'true' || import.meta.env.VITE_STUDIO_INTEGRATED === '1') || false if (INTEGRATED && resAny && resAny.token) { const serverUrl = resAny.url || resAny.studioUrl || (import.meta.env.VITE_LIVEKIT_WS_URL as string) || '' setStudioSession({ serverUrl, token: resAny.token, room: resAny.room || room }) setLoadingId(null) return } const serverUrl = resAny.url || resAny.studioUrl || (import.meta.env.VITE_LIVEKIT_WS_URL as string) || '' if (resAny.token) { setStudioSession({ serverUrl, token: resAny.token, room: resAny.room || room }) } else { setValidating(false) } setLoadingId(null) } catch (err: any) { console.error('[BroadcastPanel] Error entrando al estudio:', err) setConnectError(err?.message || 'No fue posible entrar al estudio. Revisa el servidor de tokens.') setValidating(false) setLoadingId(null) } } const closeStudio = () => { try { setStudioSession(null) } catch(e){} } if (isLoading) { return (
) } return (
{!filtered || filtered.length === 0 ? (
{activeTab === 'upcoming' ? 'No hay transmisiones programadas todavía.' : 'No hay transmisiones anteriores.' }
) : (
{filtered.map((t: Transmission) => ( ))}
Título Creado Programado
{platformIcons[t.platform] || platformIcons['YouTube']}
{t.title}
{t.platform === 'Generico' ? 'Solo grabación' : (t.platform || 'YouTube')}
{t.createdAt || '---'} {(t.scheduled && t.scheduled !== 'Próximamente') ? t.scheduled : '---'}
{launcherError && ( // Mostrar modal claro si el hook de launcher reporta un error

Error al iniciar el estudio

{launcherError}

)} } items={[ { label: 'Agregar invitados', icon: , onClick: () => { setInviteLink(`https://streamyard.com/${t.id}`); setInviteOpen(true) } }, { label: 'Editar', icon: , onClick: () => handleEdit(t) }, { divider: true, label: '', disabled: false }, { label: 'Ver en YouTube', icon: , onClick: () => {/* abrir */} }, { divider: true, label: '', disabled: false }, { label: 'Eliminar transmisión', icon: , onClick: () => onDelete(t.id), containerProps: { className: styles.deleteItem }, labelProps: { className: styles.dangerLabel } } ]} /> setInviteOpen(false)} link={inviteLink || ''} />
)} { setEditOpen(false); setEditTransmission(undefined) }} onCreate={() => {}} onUpdate={onUpdate} transmission={editTransmission} /> {studioSession && (
{ setValidating(false); /* keep portal open */ }} onRoomDisconnected={() => { closeStudio(); }} onRoomConnectError={(err) => { setValidating(false); setConnectError(String(err?.message || err || 'Error al conectar')); }} />
)} {validating && (
Validando token, por favor espera...
)} {connectError && (

Error al conectar al estudio

{connectError}

)}
) } export default TransmissionsTable