189 lines
6.2 KiB
TypeScript
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>
|
|
);
|
|
},
|
|
);
|