import React from 'react'; import { Avatar, Box, Text, Icon, Icons, config } from 'folds'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useRoomCallMembers } from '../call/useRoomCallMembers'; import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room'; import { mxcUrlToHttp, getMxIdLocalPart } from '../../utils/matrix'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { nameInitials } from '../../utils/common'; import { useCall } from '../call/useCall'; import * as css from './CallParticipantsIndicator.css'; const MAX_VISIBLE_PARTICIPANTS = 8; type CallParticipantsIndicatorProps = { roomId: string; }; export function CallParticipantsIndicator({ roomId }: CallParticipantsIndicatorProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const room = mx.getRoom(roomId); const { callMembers, isCallActive } = useRoomCallMembers(roomId); const { activeCall } = useCall(); const myUserId = mx.getUserId(); if (!isCallActive || callMembers.length === 0 || !room) { return null; } const visibleMembers = callMembers.slice(0, MAX_VISIBLE_PARTICIPANTS); const remainingCount = callMembers.length - MAX_VISIBLE_PARTICIPANTS; const getParticipantAvatar = (userId: string): string | undefined => { const mxcUrl = getMemberAvatarMxc(room, userId); if (!mxcUrl) return undefined; return mxcUrlToHttp(mx, mxcUrl, useAuthentication, 48, 48, 'crop') ?? undefined; }; const getParticipantName = (userId: string): string => { return getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId; }; /** * Check if a participant is currently speaking */ const isParticipantSpeaking = (userId: string): boolean => { if (!activeCall) return false; // Direct check for Matrix user ID (works for local user) if (activeCall.activeSpeakers.has(userId)) return true; // For remote users, activeSpeakers contains LiveKit identities (deviceId_timestamp) const member = callMembers.find((m) => m.userId === userId); if (!member) return false; // Check if any active speaker has this user's deviceId return Array.from(activeCall.activeSpeakers).some((speakerId) => { const underscoreIndex = speakerId.lastIndexOf('_'); const deviceId = underscoreIndex > 0 ? speakerId.substring(0, underscoreIndex) : speakerId; return deviceId === member.deviceId; }); }; /** * Check if a participant is currently muted */ const isParticipantMuted = (userId: string): boolean => { if (!activeCall) return false; // For local user, check activeCall.isMuted if (userId === myUserId) return activeCall.isMuted; // For remote users, mutedParticipants contains LiveKit identities (deviceId_timestamp) const member = callMembers.find((m) => m.userId === userId); if (!member) return false; // Check if any muted participant has this user's deviceId return Array.from(activeCall.mutedParticipants).some((mutedId) => { const underscoreIndex = mutedId.lastIndexOf('_'); const deviceId = underscoreIndex > 0 ? mutedId.substring(0, underscoreIndex) : mutedId; return deviceId === member.deviceId; }); }; /** * Check if the local user is deafened */ const isLocalUserDeafened = (userId: string): boolean => { if (!activeCall || userId !== myUserId) return false; return activeCall.isDeafened; }; /** * Get the appropriate audio icon for a participant */ const getAudioIcon = (userId: string): string => { if (isLocalUserDeafened(userId)) { return Icons.VolumeMute; } return isParticipantMuted(userId) ? Icons.MicMute : Icons.Mic; }; return ( {visibleMembers.map((member) => { const isSpeaking = isParticipantSpeaking(member.userId); const isMuted = isParticipantMuted(member.userId); const isDeafened = isLocalUserDeafened(member.userId); return ( {getParticipantAvatar(member.userId) ? ( {getParticipantName(member.userId)} ) : ( {nameInitials(getParticipantName(member.userId))} )} {getParticipantName(member.userId)} ); })} {remainingCount > 0 && ( +{remainingCount} more )} ); }