meet/lib/ParticipantTile.tsx
2025-04-10 01:01:04 +05:30

189 lines
6.2 KiB
TypeScript

import * as React from 'react';
import type { Participant } from 'livekit-client';
import { Track } from 'livekit-client';
import type { ParticipantClickEvent, TrackReferenceOrPlaceholder } from '@livekit/components-core';
import { isTrackReference, isTrackReferencePinned } from '@livekit/components-core';
import {
AudioTrack,
VideoTrack,
ParticipantContext,
TrackRefContext,
useEnsureTrackRef,
useFeatureContext,
useMaybeLayoutContext,
useMaybeParticipantContext,
useMaybeTrackRefContext,
useParticipantTile,
ConnectionQualityIndicator,
FocusToggle,
} from '@livekit/components-react';
import { getAvatarColor, getInitials } from './client-utils';
export function ParticipantContextIfNeeded(
props: React.PropsWithChildren<{
participant?: Participant;
}>,
) {
const hasContext = !!useMaybeParticipantContext();
return props.participant && !hasContext ? (
<ParticipantContext.Provider value={props.participant}>
{props.children}
</ParticipantContext.Provider>
) : (
<>{props.children}</>
);
}
export function TrackRefContextIfNeeded(
props: React.PropsWithChildren<{
trackRef?: TrackReferenceOrPlaceholder;
}>,
) {
const hasContext = !!useMaybeTrackRefContext();
return props.trackRef && !hasContext ? (
<TrackRefContext.Provider value={props.trackRef}>{props.children}</TrackRefContext.Provider>
) : (
<>{props.children}</>
);
}
export interface ParticipantTileProps extends React.HTMLAttributes<HTMLDivElement> {
trackRef?: TrackReferenceOrPlaceholder;
disableSpeakingIndicator?: boolean;
onParticipantClick?: (event: ParticipantClickEvent) => void;
}
export const ParticipantTile: (
props: ParticipantTileProps & React.RefAttributes<HTMLDivElement>,
) => React.ReactNode = React.forwardRef<HTMLDivElement, ParticipantTileProps>(
function ParticipantTile(
{
trackRef,
children,
onParticipantClick,
disableSpeakingIndicator,
...htmlProps
}: ParticipantTileProps,
ref,
) {
const trackReference = useEnsureTrackRef(trackRef);
const {
name,
identity,
metadata,
isEncrypted,
isSpeaking,
isMicrophoneEnabled,
isScreenShareEnabled,
} = trackReference.participant;
const { elementProps } = useParticipantTile<HTMLDivElement>({
htmlProps,
disableSpeakingIndicator,
onParticipantClick,
trackRef: trackReference,
});
const layoutContext = useMaybeLayoutContext();
const autoManageSubscription = useFeatureContext()?.autoSubscription;
const handleSubscribe = React.useCallback(
(subscribed: boolean) => {
if (
trackReference.source &&
!subscribed &&
layoutContext &&
layoutContext.pin.dispatch &&
isTrackReferencePinned(trackReference, layoutContext.pin.state)
) {
layoutContext.pin.dispatch({ msg: 'clear_pin' });
}
},
[trackReference, layoutContext],
);
const [profilePictureUrl, setProfilePictureUrl] = React.useState<string | null>(null);
React.useEffect(() => {
if (metadata) {
try {
const parsedMetadata = JSON.parse(metadata);
if (parsedMetadata.profilePictureUrl) {
setProfilePictureUrl(parsedMetadata.profilePictureUrl);
}
} catch (e) {
console.error('Failed to parse participant metadata', e);
}
}
}, [metadata]);
const avatarColor = getAvatarColor(identity);
const initials = getInitials(name || identity);
return (
<div ref={ref} style={{ position: 'relative' }} {...elementProps}>
<TrackRefContextIfNeeded trackRef={trackReference}>
<ParticipantContextIfNeeded participant={trackReference.participant}>
{children ?? (
<>
{isTrackReference(trackReference) &&
(trackReference.publication?.kind === 'video' ||
trackReference.source === Track.Source.Camera ||
trackReference.source === Track.Source.ScreenShare) ? (
<VideoTrack
trackRef={trackReference}
onSubscriptionStatusChanged={handleSubscribe}
manageSubscription={autoManageSubscription}
/>
) : (
isTrackReference(trackReference) && (
<AudioTrack
trackRef={trackReference}
onSubscriptionStatusChanged={handleSubscribe}
/>
)
)}
<div className="lk-participant-placeholder">
<div className="avatar-container" style={{ backgroundColor: avatarColor }}>
{profilePictureUrl ? (
<img src={profilePictureUrl} alt={name} className="avatar-image" />
) : (
<span className="avatar-initials">{initials}</span>
)}
</div>
</div>
<div className="lk-participant-metadata">
<div>
<div className="participant-info">
{isMicrophoneEnabled ? (
<>
{isSpeaking ? (
<span className="mic-icon speaking-icon">graphic_eq</span>
) : (
<span className="mic-icon mic-on">mic</span>
)}
</>
) : (
<span className="mic-icon mic-off">mic_off</span>
)}
<span className="participant-name">
{name || identity}
{trackReference.source === Track.Source.ScreenShare
? ' (Screen Share)'
: ''}
</span>
</div>
</div>
<ConnectionQualityIndicator className="lk-participant-metadata-item" />
</div>
</>
)}
<FocusToggle trackRef={trackReference} />
</ParticipantContextIfNeeded>
</TrackRefContextIfNeeded>
</div>
);
},
);