import { Pin, PinOff, ScreenShare } from 'lucide-react'
import { Track } from 'livekit-client'
import {
AudioTrack,
ConnectionQualityIndicator,
FocusToggle,
LockLockedIcon,
ParticipantName,
ParticipantTile,
ScreenShareIcon,
TrackMutedIndicator,
VideoTrack,
isTrackReference,
useEnsureTrackRef,
useFeatureContext,
useIsEncrypted,
useParticipantInfo,
type ParticipantClickEvent,
type TrackReferenceOrPlaceholder,
} from '@livekit/components-react'
import { Avatar } from '@/components/ui/Avatar'
import { stageTrackKey } from '@/components/room/stageFocus'
/** Метаданные участника из LiveKit access-токена (см. `AccessToken.with_metadata` на backend) — JSON `{"avatar_url": "..."}`; у гостей отсутствуют. */
interface ParticipantMetadata {
avatar_url?: string | null
}
/** Разбирает `participant.metadata` в URL аватара — `null`, если поля нет, метаданные пусты или невалидны (гость). */
function parseAvatarUrl(metadata: string | undefined): string | null {
if (!metadata) return null
try {
const parsed = JSON.parse(metadata) as ParticipantMetadata
return typeof parsed.avatar_url === 'string' && parsed.avatar_url ? parsed.avatar_url : null
} catch {
return null
}
}
/**
* Содержимое плитки — рендерится ВНУТРИ `ParticipantTile` (значит, внутри её
* `TrackRefContext`/`ParticipantContext`), поэтому берёт трек и участника из
* контекста теми же хуками, что использует сама библиотека в оригинальной
* разметке (см. `node_modules/@livekit/components-react/src/components/participant/ParticipantTile.tsx`,
* версия 2.9.23 — источник этой копии).
*/
function TileBody({ onStopSharing, pinnedKey, onTogglePin }: TileControlsProps) {
const trackReference = useEnsureTrackRef()
const isEncrypted = useIsEncrypted(trackReference.participant)
const autoManageSubscription = useFeatureContext()?.autoSubscription
// useParticipantInfo — реактивные name/metadata участника (переподписка на
// ParticipantMetadataChanged); в текущей версии metadata приходит статично
// с токена при входе и в течение сеанса не меняется, но хук — штатный
// способ читать её реактивно, если это когда-нибудь изменится.
const { name, metadata } = useParticipantInfo({ participant: trackReference.participant })
const avatarUrl = parseAvatarUrl(metadata)
const displayName = name || trackReference.participant.identity
// Чип «Вы демонстрируете экран» — только на СВОЕЙ демонстрации;
// `onStopSharing` передаётся снаружи (`RoomStage.tsx`) только для фокус-
// плитки, поэтому в карусели/гриде чип не появится даже при том же треке.
const showSharingChip = Boolean(
onStopSharing && trackReference.source === Track.Source.ScreenShare && trackReference.participant.isLocal,
)
// Кнопка закрепления — только там, где сцена умеет закрепление (основное
// окно передаёт `onTogglePin`; в мини-плеере плитка одна, закреплять нечего).
// Ключ плитки берём из её собственного трека: в карусели/гриде плитки
// рендерятся шаблоном без пропсов, снаружи «какая это плитка» не передать.
const tileKey = stageTrackKey(trackReference)
const isPinned = pinnedKey === tileKey
return (
<>
{isTrackReference(trackReference) &&
(trackReference.publication?.kind === 'video' ||
trackReference.source === Track.Source.Camera ||
trackReference.source === Track.Source.ScreenShare) ? (