From 32be1bceabd4a35a84100654eb0a40745edc1744 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:25:41 +0100 Subject: [PATCH] feat: add voice participant context menus on mobile Long-press on voice participants shows moderation items (space mute, deafen, disconnect, move) and local audio controls (mute, volume). Skips self and DM calls. --- .../layout/MobileVoiceFullScreen.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/web/src/components/layout/MobileVoiceFullScreen.tsx b/packages/web/src/components/layout/MobileVoiceFullScreen.tsx index e2164f02..a5f4f950 100644 --- a/packages/web/src/components/layout/MobileVoiceFullScreen.tsx +++ b/packages/web/src/components/layout/MobileVoiceFullScreen.tsx @@ -4,6 +4,8 @@ import { useVoiceStore } from '../../stores/voiceStore'; import { useSpaceStore } from '../../stores/spaceStore'; import { useAuthStore } from '../../stores/authStore'; import { Avatar } from '../ui/Avatar'; +import { useContextMenuStore, type ContextMenuItem } from '../../stores/contextMenuStore'; +import { buildVoiceModMenuItems, VolumeSliderItem } from '../voice/voiceMenuItems'; export function MobileVoiceFullScreen() { const popMobileScreen = useUIStore((s) => s.popMobileScreen); @@ -30,6 +32,8 @@ export function MobileVoiceFullScreen() { const authUser = useAuthStore((s) => s.user); + const openContextMenu = useContextMenuStore((s) => s.open); + if (!currentVoiceChannelId) { popMobileScreen(); return null; @@ -87,6 +91,41 @@ export function MobileVoiceFullScreen() { popMobileScreen(); }; + const handleParticipantContextMenu = (e: React.MouseEvent, userId: string) => { + if (userId === authUser?.id || !currentVoiceChannelId || isDmCall) return; + e.preventDefault(); + e.stopPropagation(); + + const modItems = buildVoiceModMenuItems(userId, currentVoiceChannelId); + const items: ContextMenuItem[] = [...modItems]; + + if (modItems.length > 0) { + items.push({ key: 'mod-end-sep', type: 'separator' }); + } + + // Local mute checkbox + const isUserMuted = useVoiceStore.getState().participantMutes.get(userId) ?? false; + items.push({ + key: 'mute-user', + type: 'checkbox', + label: 'Mute User', + checked: isUserMuted, + onChange: (checked) => useVoiceStore.getState().setParticipantMute(userId, checked), + }); + + items.push({ key: 'vol-sep', type: 'separator' }); + + // Volume slider + items.push({ + key: 'volume', + type: 'custom', + render: () => , + }); + + if (items.length === 0) return; + openContextMenu({ x: e.clientX, y: e.clientY }, items); + }; + return (
{/* Header */} @@ -129,6 +168,7 @@ export function MobileVoiceFullScreen() { className={`rounded-xl bg-surface-channel p-4 flex flex-col items-center gap-3 ${ participantIds.length <= 2 ? 'py-8' : 'py-4' }`} + onContextMenu={(e) => handleParticipantContextMenu(e, userId)} >