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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user