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) ? ( ) : ( isTrackReference(trackReference) && )} {/* Вместо штатной иконки-заглушки (ParticipantPlaceholder) — аватар участника: фото по avatar_url из метаданных, фолбэк — инициалы имени (см. `ui/Avatar`). Видимость (opacity) этого блока при выключенной камере управляется тем же CSS-правилом библиотеки (`[data-lk-video-muted='true'][data-lk-source='camera'] .lk-participant-placeholder`) — data-атрибуты на корневой div выставляет сам `ParticipantTile` независимо от children. */}
{trackReference.source === Track.Source.Camera ? ( <> {isEncrypted && } ) : ( <> {/* `ParticipantName` рендерит `participant.name` (или identity), а `children` — необязательный суффикс ПОСЛЕ имени; оригинал дописывал сюда английское `'s screen` («Имя's screen»), здесь — русский суффикс («Имя — демонстрация экрана»). */} {' — демонстрация экрана'} )}
{onTogglePin && ( )} {showSharingChip && (
)} ) } /** Управляющие элементы поверх плитки — общие для обёртки и её `TileBody`. */ interface TileControlsProps { /** * Остановить демонстрацию экрана — если передано, при рендере СВОЕЙ активной * демонстрации (Track.Source.ScreenShare + `participant.isLocal`) поверх * плитки показывается чип «Вы демонстрируете экран» с кнопкой «Остановить» * Передавать только для фокус-плитки — в карусели демонстрация * в этом приложении не появляется (см. `RoomStage.tsx`). */ onStopSharing?: () => void /** * Ключ закреплённой сейчас плитки (`identity:source`, см. `stageTrackKey`) — * плитка сравнивает его со своим и подсвечивает булавку/рамку. */ pinnedKey?: string | null /** * Закрепить/открепить эту плитку в основном окне. Передаёт свой ключ * (плитки в карусели/гриде рендерятся шаблоном, снаружи их не различить). * Не передан — кнопки-булавки на плитке нет (мини-плеер: плитка одна). */ onTogglePin?: (key: string) => void } interface RoomParticipantTileProps extends TileControlsProps { trackRef?: TrackReferenceOrPlaceholder disableSpeakingIndicator?: boolean onParticipantClick?: (event: ParticipantClickEvent) => void } /** * Обёртка над штатным `ParticipantTile` (@livekit/components-react 2.9.23, * версия запинена в package.json — при апгрейде библиотеки сверить разметку * заново): та же структура (видео/аудио-трек, блок метаданных с именем и индикатором мьюта, * FocusToggle), но вместо стандартной SVG-пиктограммы при выключенной камере — * аватар участника. Работает и для локального участника (свои метаданные из * собственного токена), и для удалённых. * * Пин-логика оригинала (`handleSubscribe`/сброс пина при отписке от трека) * сознательно опущена — приложение нигде не создаёт `LayoutContext`, поэтому * в оригинале эта ветка и так была мёртвым кодом без провайдера контекста (по * той же причине ничего не рисует и штатный `FocusToggle` ниже). Своё * закрепление участника (задача 3.1) сделано мимо `LayoutContext`: состояние — * в `RoomStage`, кнопка — `.room-pin-toggle` здесь, выбор фокуса — * `pickStageFocus`. */ export function RoomParticipantTile({ trackRef, disableSpeakingIndicator, onParticipantClick, onStopSharing, pinnedKey, onTogglePin, }: RoomParticipantTileProps) { return ( ) }