feat(voice): open the profile card from voice participants
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
The profile popout already existed and was reachable from eleven places — messages, mentions, avatars, member list, DMs, activity panel — but no voice surface opened it, so clicking someone during a call did nothing. Wire it into the voice user rows (VoiceChannel's sidebar list) and the name label on grid tiles, whose avatar was already a ProfileAvatar; the name beside it not reacting read as the click failing. Left mobile alone deliberately: MobileSpacesScreen already opens the profile from its row wrapper, and MobileVoiceJoinSheet would layer a history-pushed full-screen profile inside a bottom sheet, which cannot be verified here.
This commit is contained in:
@@ -1,8 +1,18 @@
|
||||
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;
|
||||
@@ -22,6 +32,7 @@ export interface VoiceUserRowProps {
|
||||
export function VoiceUserRow({
|
||||
userId,
|
||||
displayName,
|
||||
user,
|
||||
avatar,
|
||||
avatarColor,
|
||||
isMuted,
|
||||
@@ -38,6 +49,15 @@ export function VoiceUserRow({
|
||||
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;
|
||||
@@ -49,17 +69,38 @@ export function VoiceUserRow({
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-2 ${className}`}>
|
||||
<Avatar
|
||||
src={avatar}
|
||||
name={displayName}
|
||||
size={avatarSize}
|
||||
userId={userId}
|
||||
avatarColor={avatarColor}
|
||||
className={isSpeaking ? 'rounded-full ring-2 ring-status-online' : ''}
|
||||
/>
|
||||
<span className="text-[13px] text-txt-secondary truncate flex-1 min-w-0">
|
||||
{displayName}
|
||||
</span>
|
||||
{user ? (
|
||||
<ProfileAvatar
|
||||
src={avatar}
|
||||
name={displayName}
|
||||
size={avatarSize}
|
||||
userId={userId}
|
||||
user={user}
|
||||
avatarColor={avatarColor}
|
||||
className={isSpeaking ? 'rounded-full ring-2 ring-status-online' : ''}
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
src={avatar}
|
||||
name={displayName}
|
||||
size={avatarSize}
|
||||
userId={userId}
|
||||
avatarColor={avatarColor}
|
||||
className={isSpeaking ? 'rounded-full ring-2 ring-status-online' : ''}
|
||||
/>
|
||||
)}
|
||||
{user ? (
|
||||
<button
|
||||
onClick={openProfile}
|
||||
className="text-[13px] text-txt-secondary truncate flex-1 min-w-0 text-left hover:text-txt-primary hover:underline"
|
||||
>
|
||||
{displayName}
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-[13px] text-txt-secondary truncate flex-1 min-w-0">
|
||||
{displayName}
|
||||
</span>
|
||||
)}
|
||||
{/* Status badges */}
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{/* Server muted / space deafened / permission muted — amber mic with slash */}
|
||||
|
||||
Reference in New Issue
Block a user