meet/lib/ParticipantTile.tsx
2025-03-18 20:32:21 +05:30

133 lines
3.7 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import {
AudioTrack,
useTracks,
VideoTrack,
useTrackRefContext,
useEnsureTrackRef,
TrackRefContextIfNeeded,
} from '@livekit/components-react';
import { Track, Participant } from 'livekit-client';
import { isTrackReference } from '@livekit/components-core';
function getAvatarColor(identity: string): string {
const colors = [
'#4CAF50',
'#8BC34A',
'#CDDC39',
'#FFC107',
'#FF9800',
'#FF5722',
'#F44336',
'#E91E63',
'#9C27B0',
'#673AB7',
'#3F51B5',
'#2196F3',
'#03A9F4',
'#00BCD4',
'#009688',
];
let hash = 0;
for (let i = 0; i < identity.length; i++) {
hash = identity.charCodeAt(i) + ((hash << 5) - hash);
}
const index = Math.abs(hash) % colors.length;
return colors[index];
}
function getInitials(name: string): string {
if (!name) return '?';
const parts = name.split(' ');
if (parts.length === 1) {
return parts[0].charAt(0).toUpperCase();
}
return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
}
export interface ParticipantTileProps {
participant?: Participant;
}
export const ParticipantTile: React.FC<ParticipantTileProps> = ({
participant: propParticipant,
}) => {
const trackRef = useTrackRefContext();
const trackReference = useEnsureTrackRef(trackRef);
const participant = propParticipant || trackRef?.participant;
if (!participant) return null;
const [profilePictureUrl, setProfilePictureUrl] = useState<string | null>(null);
const microphoneTrack = useTracks([Track.Source.Microphone], { onlySubscribed: false }).filter(
(track) => track.participant.identity === participant.identity,
)[0];
const isSpeaking = participant.isSpeaking;
useEffect(() => {
if (participant.metadata) {
try {
const metadata = JSON.parse(participant.metadata);
if (metadata.profilePictureUrl) {
setProfilePictureUrl(metadata.profilePictureUrl);
}
} catch (e) {
console.error('Failed to parse participant metadata', e);
}
}
}, [participant.metadata]);
const isCameraEnabled =
(trackReference.source === Track.Source.Camera && !trackReference.publication?.isMuted) ||
trackReference.source === Track.Source.ScreenShare;
const hasMicrophone = !!microphoneTrack;
const isMicrophoneEnabled = hasMicrophone && !microphoneTrack.publication?.isMuted;
const avatarColor = getAvatarColor(participant.identity);
const initials = getInitials(participant.name || participant.identity);
return (
<div className={`participant-tile ${isSpeaking ? 'speaking' : ''}`}>
{isTrackReference(trackReference) && isCameraEnabled ? (
<div className="video-container">
<VideoTrack trackRef={trackReference} />
</div>
) : (
<div className="avatar-container" style={{ backgroundColor: avatarColor }}>
{profilePictureUrl ? (
<img src={profilePictureUrl} alt={participant.name} className="avatar-image" />
) : (
<span className="avatar-initials">{initials}</span>
)}
</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">{participant.name || participant.identity}</span>
</div>
{hasMicrophone && microphoneTrack && <AudioTrack trackRef={microphoneTrack} />}
</div>
);
};
export default ParticipantTile;