From 37407a5ecd385826286c27c4723bd862d37f2901 Mon Sep 17 00:00:00 2001 From: devsyncwrld Date: Mon, 31 Aug 2026 11:49:57 -0300 Subject: [PATCH] feat(voice): open the profile card from voice participants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/roadmap-resenha.md | 7 ++- .../web/src/components/voice/VoiceChannel.tsx | 1 + .../web/src/components/voice/VoiceUser.tsx | 26 ++++++-- .../web/src/components/voice/VoiceUserRow.tsx | 63 +++++++++++++++---- 4 files changed, 80 insertions(+), 17 deletions(-) diff --git a/docs/roadmap-resenha.md b/docs/roadmap-resenha.md index 88765a57..3c1c3b06 100644 --- a/docs/roadmap-resenha.md +++ b/docs/roadmap-resenha.md @@ -13,6 +13,7 @@ código e os commits seguem em inglês, como o resto do repositório. | Ir para a call clicando no nome do canal | `c70b0095` | Exigiu o `voiceStore` passar a guardar o espaço da call — antes ele não sabia onde a call estava assim que o usuário navegava para outro servidor | | Botão de GIF redesenhado | `20526e1b` | Contorno vazado com letras cheias, no lugar do bloco sólido | | Explorador de GIF no banner | `20526e1b` | Sem upload: banner já aceita URL absoluta no cliente e no servidor | +| Preview de perfil nos participantes da call | (ver git log) | O popout já existia e era aberto de 11 lugares; **nenhum era de voz**. Ligado nas linhas da lista de voz e no nome dos tiles da grade | ## Já existia no código (verificado, não construir de novo) @@ -21,6 +22,11 @@ código e os commits seguem em inglês, como o resto do repositório. - **Sons de call/stream** — `SoundController.tsx`, montado no `AppLayout`: stream started/ended, alguém entra/sai da tela, entra/sai da call, câmera, mute, ringing. Os `.ogg` estão em `web/public/sounds/`. +- **Preview de perfil ancorado** — `uiStore.openUserProfile(user, anchor, placement)` + guarda `userProfilePopout`; popout posicionado no desktop, tela cheia no + mobile. Já era aberto por mensagens, menções, avatares, lista de membros, + DMs e painel de atividade. O que faltava era só a voz — agora ligado. + Continua faltando o bloco de atividade do print do Discord, que é a #8. Se qualquer um dos dois não se manifestar em uso, o trabalho é **depuração**, não implementação. @@ -31,7 +37,6 @@ Ordem sugerida: as pequenas primeiro, as grandes uma de cada vez. | # | Feature | Tamanho | Observação técnica | |---|---|---|---| -| 7 | Preview de perfil ao clicar no nome | Média | Os dados já vêm do `UserProfileModal`; é transformar em popover ancorado | | 10 | Teste de voz com loopback | Média | `AudioManager.playTestTone()` já existe; falta capturar o mic e devolver no monitor, com "Stop Testing" | | 6 | Favoritar GIFs + categorias | Grande | Precisa de tabela, migração drizzle e API para sincronizar entre dispositivos, como no Discord | | 8 | Atividade (Spotify etc.) | Grande | `activityStore` e `activityBridge` já existem, mas a detecção é via Electron; Spotify exige OAuth e presença via WebSocket | diff --git a/packages/web/src/components/voice/VoiceChannel.tsx b/packages/web/src/components/voice/VoiceChannel.tsx index 48951e1e..911d1cdd 100644 --- a/packages/web/src/components/voice/VoiceChannel.tsx +++ b/packages/web/src/components/voice/VoiceChannel.tsx @@ -199,6 +199,7 @@ export function VoiceChannel({ channelId, channelName, onClick, locked, canManag s.openUserProfile); // --- VIDEO & UI --- @@ -177,11 +179,25 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
- - {displayName} - + {user ? ( + // The tile's avatar already opens the profile card; the name + // sitting next to it did not, which read as the click failing. + + ) : ( + + {displayName} + + )} {isLocal && ( (you) diff --git a/packages/web/src/components/voice/VoiceUserRow.tsx b/packages/web/src/components/voice/VoiceUserRow.tsx index f2d2c6ca..c221bdc7 100644 --- a/packages/web/src/components/voice/VoiceUserRow.tsx +++ b/packages/web/src/components/voice/VoiceUserRow.tsx @@ -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 (
- - - {displayName} - + {user ? ( + + ) : ( + + )} + {user ? ( + + ) : ( + + {displayName} + + )} {/* Status badges */}
{/* Server muted / space deafened / permission muted — amber mic with slash */}