import React, { useEffect, useMemo, useState } from 'react';
import { VoiceUser } from './VoiceUser';
import { StreamTile } from './StreamTile';
import { useVoiceStore } from '../../stores/voiceStore';
import { deriveGridTiles } from '../../hooks/useLiveKit';
import { useGridLayout } from '../../hooks/useGridLayout';
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 [stripHidden, setStripHidden] = useState(false);
const tiles = useMemo(() => deriveGridTiles(participants), [participants]);
const { cols, tileWidth, tileHeight, ref: gridRef } = useGridLayout(tiles.length);
// Reset strip visibility when focus target changes
useEffect(() => {
setStripHidden(false);
}, [focusedParticipantId]);
// 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)}
{/* Grid button — top-right */}
{/* Centered Hide/Show Members button — divider between focused tile and strip */}
{otherTiles.length > 0 && (
)}
{/* Bottom strip of other tiles */}
{!stripHidden && 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 — container-aware layout via ResizeObserver
return (
{tiles.map((t) => (
setFocusedParticipant(t.key)}
className="cursor-pointer hover:opacity-90 transition-opacity"
>
{renderTile(t)}
))}
);
}