import type { MouseEvent as ReactMouseEvent } from 'react'; import type { User } from '@backspace/shared'; import { Avatar } from '../ui/Avatar'; import { ProfileAvatar } from '../ui/ProfileAvatar'; import { useUIStore } from '../../stores/uiStore'; export interface VoiceUserRowProps { userId: string; displayName: string; /** * Full record for the participant, when it has resolved. Without it the row * stays presentational rather than offering a click that opens nothing — * the same contract ProfileAvatar documents. */ user?: User; avatar: string | null; avatarColor?: string; isMuted?: boolean; isDeafened?: boolean; isCameraOn?: boolean; isUnwatchedCamera?: boolean; isScreenSharing?: boolean; isServerMuted?: boolean; isServerDeafened?: boolean; isPermissionMuted?: boolean; isLocallyMuted?: boolean; isSpeaking?: boolean; size?: 'compact' | 'default'; className?: string; } export function VoiceUserRow({ userId, displayName, user, avatar, avatarColor, isMuted, isDeafened, isCameraOn, isUnwatchedCamera, isScreenSharing, isServerMuted, isServerDeafened, isPermissionMuted, isLocallyMuted, isSpeaking, size = 'default', className = '', }: VoiceUserRowProps) { const avatarSize = size === 'compact' ? 20 : 24; const openUserProfile = useUIStore((s) => s.openUserProfile); const openProfile = (e: ReactMouseEvent) => { if (!user) return; // The row sits inside a channel that has its own click target (join the // call). Opening the profile is the more specific intent. e.stopPropagation(); openUserProfile(user, e.currentTarget.getBoundingClientRect(), 'right'); }; // Mic icon priority: server muted/deafened/permission muted (amber) > self-muted (danger) const showServerMicIcon = isServerMuted || isServerDeafened || isPermissionMuted; const showSelfMicIcon = !showServerMicIcon && isMuted; // Deafen icon priority: server deafened (amber) > self-deafened (danger) const showServerDeafIcon = isServerDeafened; const showSelfDeafIcon = !isServerDeafened && isDeafened; return (
{user ? ( ) : ( )} {user ? ( ) : ( {displayName} )} {/* Status badges */}
{/* Server muted / space deafened / permission muted — amber mic with slash */} {showServerMicIcon && ( )} {/* Server deafened — amber headphone with slash */} {showServerDeafIcon && ( )} {/* Self-muted — danger mic with slash */} {showSelfMicIcon && ( )} {/* Self-deafened — danger headphone with slash */} {showSelfDeafIcon && ( )} {/* Camera active */} {isCameraOn && ( {isUnwatchedCamera && ( )} )} {/* Screen sharing LIVE badge */} {isScreenSharing && ( LIVE )} {/* Locally muted — volume X icon */} {isLocallyMuted && ( )}
); }