import React, { useEffect, useMemo } from 'react'; import { VoiceUser } from './VoiceUser'; import { StreamTile } from './StreamTile'; import { useVoiceStore } from '../../stores/voiceStore'; import { deriveGridTiles } from '../../hooks/useLiveKit'; import type { ParticipantInfo, GridTile } from '../../hooks/useLiveKit'; interface VoiceGridProps { participants: ParticipantInfo[]; } export function VoiceGrid({ participants }: VoiceGridProps) { const focusedParticipantId = useVoiceStore((s) => s.focusedParticipantId); const setFocusedParticipant = useVoiceStore((s) => s.setFocusedParticipant); const tiles = useMemo(() => deriveGridTiles(participants), [participants]); // Unfocus if the focused stream tile no longer exists useEffect(() => { const currentStreamKeys = new Set( tiles .filter( (t): t is GridTile & { kind: 'stream' } => t.kind === 'stream' && t.screenTrack?.readyState === 'live', ) .map((t) => t.key), ); if ( focusedParticipantId && focusedParticipantId.endsWith(':stream') && !currentStreamKeys.has(focusedParticipantId) ) { setFocusedParticipant(null); } }, [tiles, focusedParticipantId, setFocusedParticipant]); if (tiles.length === 0) { return (

Waiting for others to join...

); } const focusedTile = focusedParticipantId ? tiles.find((t) => t.key === focusedParticipantId) : null; // Render a single tile polymorphically const renderTile = (tile: GridTile, large?: boolean) => tile.kind === 'user' ? ( ) : ( ); // Focus mode: one large tile + bottom strip if (focusedTile) { const otherTiles = tiles.filter((t) => t.key !== focusedParticipantId); return (
{/* Main focused view */}
setFocusedParticipant(null)} title="Click to return to grid view" > {renderTile(focusedTile, true)} {/* Back to grid button */}
{/* Bottom strip of other tiles */} {otherTiles.length > 0 && (
{otherTiles.map((t) => (
setFocusedParticipant(t.key)} className="h-full aspect-video flex-shrink-0 cursor-pointer hover:opacity-80 transition-opacity" > {renderTile(t)}
))}
)}
); } // Default grid mode const gridClass = (() => { if (tiles.length === 1) return 'grid-cols-1 max-w-2xl mx-auto'; if (tiles.length === 2) return 'grid-cols-2 max-w-4xl mx-auto'; if (tiles.length <= 4) return 'grid-cols-2'; if (tiles.length <= 9) return 'grid-cols-3'; return 'grid-cols-4'; })(); return (
{tiles.map((t) => (
setFocusedParticipant(t.key)} className="cursor-pointer hover:opacity-90 transition-opacity h-full" > {renderTile(t)}
))}
); }