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.
This commit is contained in:
Jannis Braun
2026-03-23 19:25:41 +01:00
parent 937c4b68b2
commit 32be1bceab
@@ -4,6 +4,8 @@ import { useVoiceStore } from '../../stores/voiceStore';
import { useSpaceStore } from '../../stores/spaceStore'; import { useSpaceStore } from '../../stores/spaceStore';
import { useAuthStore } from '../../stores/authStore'; import { useAuthStore } from '../../stores/authStore';
import { Avatar } from '../ui/Avatar'; import { Avatar } from '../ui/Avatar';
import { useContextMenuStore, type ContextMenuItem } from '../../stores/contextMenuStore';
import { buildVoiceModMenuItems, VolumeSliderItem } from '../voice/voiceMenuItems';
export function MobileVoiceFullScreen() { export function MobileVoiceFullScreen() {
const popMobileScreen = useUIStore((s) => s.popMobileScreen); const popMobileScreen = useUIStore((s) => s.popMobileScreen);
@@ -30,6 +32,8 @@ export function MobileVoiceFullScreen() {
const authUser = useAuthStore((s) => s.user); const authUser = useAuthStore((s) => s.user);
const openContextMenu = useContextMenuStore((s) => s.open);
if (!currentVoiceChannelId) { if (!currentVoiceChannelId) {
popMobileScreen(); popMobileScreen();
return null; return null;
@@ -87,6 +91,41 @@ export function MobileVoiceFullScreen() {
popMobileScreen(); 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: () => <VolumeSliderItem userId={userId} />,
});
if (items.length === 0) return;
openContextMenu({ x: e.clientX, y: e.clientY }, items);
};
return ( return (
<div className="flex flex-col h-full bg-surface-base"> <div className="flex flex-col h-full bg-surface-base">
{/* Header */} {/* Header */}
@@ -129,6 +168,7 @@ export function MobileVoiceFullScreen() {
className={`rounded-xl bg-surface-channel p-4 flex flex-col items-center gap-3 ${ className={`rounded-xl bg-surface-channel p-4 flex flex-col items-center gap-3 ${
participantIds.length <= 2 ? 'py-8' : 'py-4' participantIds.length <= 2 ? 'py-8' : 'py-4'
}`} }`}
onContextMenu={(e) => handleParticipantContextMenu(e, userId)}
> >
<div className="relative"> <div className="relative">
<Avatar <Avatar