feat: implement sidebar docked call panel with participant management and sync status handling
This commit is contained in:
10
src/app/features/room-nav/CallParticipantsIndicator.css.ts
Normal file
10
src/app/features/room-nav/CallParticipantsIndicator.css.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { color } from 'folds';
|
||||
|
||||
/**
|
||||
* Style for participants who are currently speaking
|
||||
* Adds a green glow effect to indicate voice activity
|
||||
*/
|
||||
export const ParticipantSpeaking = style({
|
||||
boxShadow: `0 0 0 2px ${color.Success.Main}`,
|
||||
});
|
||||
@@ -7,6 +7,7 @@ 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;
|
||||
|
||||
@@ -20,6 +21,7 @@ export function CallParticipantsIndicator({ roomId }: CallParticipantsIndicatorP
|
||||
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;
|
||||
@@ -38,14 +40,37 @@ export function CallParticipantsIndicator({ roomId }: CallParticipantsIndicatorP
|
||||
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 === mx.getUserId()) return activeCall.isMuted;
|
||||
if (userId === myUserId) return activeCall.isMuted;
|
||||
|
||||
// For remote users, mutedParticipants contains LiveKit identities (deviceId_timestamp)
|
||||
// We need to find the deviceId for this user and check if any matching identity is muted
|
||||
const member = callMembers.find((m) => m.userId === userId);
|
||||
if (!member) return false;
|
||||
|
||||
@@ -57,6 +82,24 @@ export function CallParticipantsIndicator({ roomId }: CallParticipantsIndicatorP
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<Box
|
||||
direction="Column"
|
||||
@@ -67,45 +110,58 @@ export function CallParticipantsIndicator({ roomId }: CallParticipantsIndicatorP
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{visibleMembers.map((member) => (
|
||||
<Box
|
||||
key={member.userId}
|
||||
alignItems="Center"
|
||||
gap="100"
|
||||
style={{
|
||||
opacity: config.opacity.P300,
|
||||
}}
|
||||
>
|
||||
<Avatar size="200" radii="Pill">
|
||||
{getParticipantAvatar(member.userId) ? (
|
||||
<img
|
||||
src={getParticipantAvatar(member.userId)}
|
||||
alt={getParticipantName(member.userId)}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
borderRadius: 'inherit',
|
||||
}}
|
||||
{visibleMembers.map((member) => {
|
||||
const isSpeaking = isParticipantSpeaking(member.userId);
|
||||
const isMuted = isParticipantMuted(member.userId);
|
||||
const isDeafened = isLocalUserDeafened(member.userId);
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={member.userId}
|
||||
alignItems="Center"
|
||||
gap="100"
|
||||
style={{
|
||||
opacity: config.opacity.P300,
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
size="200"
|
||||
radii="Pill"
|
||||
className={isSpeaking ? css.ParticipantSpeaking : undefined}
|
||||
>
|
||||
{getParticipantAvatar(member.userId) ? (
|
||||
<img
|
||||
src={getParticipantAvatar(member.userId)}
|
||||
alt={getParticipantName(member.userId)}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
borderRadius: 'inherit',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Text as="span" size="H6">
|
||||
{nameInitials(getParticipantName(member.userId))}
|
||||
</Text>
|
||||
)}
|
||||
</Avatar>
|
||||
<Box alignItems="Center" gap="100" grow="Yes">
|
||||
<Icon
|
||||
src={getAudioIcon(member.userId)}
|
||||
size="50"
|
||||
style={{
|
||||
opacity: config.opacity.P500,
|
||||
color: isDeafened ? 'var(--color-Critical-Main)' : isMuted ? 'var(--color-Critical-Main)' : undefined
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Text as="span" size="H6">
|
||||
{nameInitials(getParticipantName(member.userId))}
|
||||
<Text as="span" size="T200" truncate>
|
||||
{getParticipantName(member.userId)}
|
||||
</Text>
|
||||
)}
|
||||
</Avatar>
|
||||
<Box alignItems="Center" gap="100" grow="Yes">
|
||||
<Icon
|
||||
src={isParticipantMuted(member.userId) ? Icons.MicMute : Icons.Mic}
|
||||
size="50"
|
||||
style={{ opacity: config.opacity.P500 }}
|
||||
/>
|
||||
<Text as="span" size="T200" truncate>
|
||||
{getParticipantName(member.userId)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
{remainingCount > 0 && (
|
||||
<Box
|
||||
alignItems="Center"
|
||||
|
||||
Reference in New Issue
Block a user