refactor(voice): extract action handlers into voiceActions.ts, remove hard-coded M/D shortcuts
Move mute/deafen/camera/screen-share/disconnect logic out of VoiceControlBar into a shared voiceActions.ts utility so the same handlers can be called from both UI buttons and the upcoming keybind dispatcher.
This commit is contained in:
@@ -2,13 +2,10 @@ import React, { useEffect, useState, useRef } from 'react';
|
|||||||
import { useVoiceStore } from '../../stores/voiceStore';
|
import { useVoiceStore } from '../../stores/voiceStore';
|
||||||
import { useUIStore } from '../../stores/uiStore';
|
import { useUIStore } from '../../stores/uiStore';
|
||||||
import { useAuthStore } from '../../stores/authStore';
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
import { getActiveRoom } from '../../hooks/useLiveKit';
|
|
||||||
import { wsSend } from '../../hooks/useWebSocket';
|
|
||||||
import { useSpaceStore, getChannelOrigin, getMyUserIdForOrigin } from '../../stores/spaceStore';
|
import { useSpaceStore, getChannelOrigin, getMyUserIdForOrigin } from '../../stores/spaceStore';
|
||||||
import { ScreenShareSettingsPopover } from './ScreenShareSettingsPopover';
|
import { ScreenShareSettingsPopover } from './ScreenShareSettingsPopover';
|
||||||
import { CAMERA_PRESET, startScreenShare, stopScreenShare } from '../../utils/screenShare';
|
|
||||||
import { broadcastVoiceStatus, broadcastDeafenViaLiveKit } from '../../utils/voice';
|
|
||||||
import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
|
import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
|
||||||
|
import { handleMuteAction, handleDeafenAction, handleCameraAction, handleScreenShareAction, handleDisconnectAction } from '../../utils/voiceActions';
|
||||||
|
|
||||||
const btnBase = 'w-10 h-10 flex items-center justify-center rounded-full transition-colors';
|
const btnBase = 'w-10 h-10 flex items-center justify-center rounded-full transition-colors';
|
||||||
const btnDefault = `${btnBase} bg-surface-channel text-txt-secondary hover:bg-surface-elevated hover:text-txt-primary`;
|
const btnDefault = `${btnBase} bg-surface-channel text-txt-secondary hover:bg-surface-elevated hover:text-txt-primary`;
|
||||||
@@ -20,9 +17,6 @@ export function VoiceControlBar() {
|
|||||||
const isDeafened = useVoiceStore((s) => s.isDeafened);
|
const isDeafened = useVoiceStore((s) => s.isDeafened);
|
||||||
const isCameraOn = useVoiceStore((s) => s.isCameraOn);
|
const isCameraOn = useVoiceStore((s) => s.isCameraOn);
|
||||||
const isScreenSharing = useVoiceStore((s) => s.isScreenSharing);
|
const isScreenSharing = useVoiceStore((s) => s.isScreenSharing);
|
||||||
const toggleMic = useVoiceStore((s) => s.toggleMic);
|
|
||||||
const toggleDeafen = useVoiceStore((s) => s.toggleDeafen);
|
|
||||||
const toggleCamera = useVoiceStore((s) => s.toggleCamera);
|
|
||||||
const voiceChatOpen = useUIStore((s) => s.voiceChatOpen);
|
const voiceChatOpen = useUIStore((s) => s.voiceChatOpen);
|
||||||
const toggleVoiceChat = useUIStore((s) => s.toggleVoiceChat);
|
const toggleVoiceChat = useUIStore((s) => s.toggleVoiceChat);
|
||||||
const voiceFullscreen = useUIStore((s) => s.voiceFullscreen);
|
const voiceFullscreen = useUIStore((s) => s.voiceFullscreen);
|
||||||
@@ -30,7 +24,6 @@ export function VoiceControlBar() {
|
|||||||
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
||||||
const myUser = useAuthStore((s) => s.user);
|
const myUser = useAuthStore((s) => s.user);
|
||||||
const spaceId = useSpaceStore((s) => currentVoiceChannelId ? s.channelToSpaceMap.get(currentVoiceChannelId) : null);
|
const spaceId = useSpaceStore((s) => currentVoiceChannelId ? s.channelToSpaceMap.get(currentVoiceChannelId) : null);
|
||||||
const voiceOrigin = currentVoiceChannelId ? getChannelOrigin(currentVoiceChannelId) : '';
|
|
||||||
const myOriginId = useSpaceStore((s) => currentVoiceChannelId ? getMyUserIdForOrigin(getChannelOrigin(currentVoiceChannelId)) : s.members.find(m => m.userId === myUser?.id)?.userId ?? myUser?.id);
|
const myOriginId = useSpaceStore((s) => currentVoiceChannelId ? getMyUserIdForOrigin(getChannelOrigin(currentVoiceChannelId)) : s.members.find(m => m.userId === myUser?.id)?.userId ?? myUser?.id);
|
||||||
const spaceMutedUserIds = useVoiceStore((s) => s.spaceMutedUserIds);
|
const spaceMutedUserIds = useVoiceStore((s) => s.spaceMutedUserIds);
|
||||||
const spaceDeafenedUserIds = useVoiceStore((s) => s.spaceDeafenedUserIds);
|
const spaceDeafenedUserIds = useVoiceStore((s) => s.spaceDeafenedUserIds);
|
||||||
@@ -44,102 +37,33 @@ export function VoiceControlBar() {
|
|||||||
const [qualityOpen, setQualityOpen] = useState(false);
|
const [qualityOpen, setQualityOpen] = useState(false);
|
||||||
const qualityBtnRef = useRef<HTMLButtonElement>(null);
|
const qualityBtnRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const handleMute = React.useCallback(async () => {
|
const handleMute = React.useCallback(() => {
|
||||||
if (isSpaceMuted || isSpaceDeafened) return;
|
handleMuteAction(isSpaceMuted, isSpaceDeafened);
|
||||||
const wasDeafened = useVoiceStore.getState().isDeafened;
|
}, [isSpaceMuted, isSpaceDeafened]);
|
||||||
toggleMic();
|
|
||||||
broadcastVoiceStatus();
|
|
||||||
// If unmuting while deafened cleared deafen, broadcast via LiveKit data channel
|
|
||||||
if (wasDeafened && !useVoiceStore.getState().isDeafened) {
|
|
||||||
broadcastDeafenViaLiveKit();
|
|
||||||
}
|
|
||||||
}, [isSpaceMuted, isSpaceDeafened, toggleMic]);
|
|
||||||
|
|
||||||
const handleDeafen = React.useCallback(async () => {
|
const handleDeafen = React.useCallback(() => {
|
||||||
if (isSpaceDeafened) return;
|
handleDeafenAction(isSpaceDeafened);
|
||||||
toggleDeafen();
|
}, [isSpaceDeafened]);
|
||||||
broadcastVoiceStatus();
|
|
||||||
broadcastDeafenViaLiveKit();
|
|
||||||
}, [isSpaceDeafened, toggleDeafen]);
|
|
||||||
|
|
||||||
const handleCamera = async () => {
|
const handleCamera = () => handleCameraAction();
|
||||||
const room = getActiveRoom();
|
|
||||||
if (!room) return;
|
|
||||||
try {
|
|
||||||
const willEnable = !isCameraOn;
|
|
||||||
if (willEnable) {
|
|
||||||
await room.localParticipant.setCameraEnabled(true,
|
|
||||||
{ resolution: CAMERA_PRESET.resolution },
|
|
||||||
{
|
|
||||||
videoCodec: CAMERA_PRESET.codec,
|
|
||||||
videoEncoding: CAMERA_PRESET.encoding,
|
|
||||||
simulcast: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await room.localParticipant.setCameraEnabled(false);
|
|
||||||
}
|
|
||||||
toggleCamera();
|
|
||||||
broadcastVoiceStatus();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[VoiceControlBar] Failed to toggle camera:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleScreenShare = async () => {
|
const handleScreenShare = () => handleScreenShareAction();
|
||||||
const room = getActiveRoom();
|
|
||||||
if (!room) return;
|
|
||||||
try {
|
|
||||||
if (!isScreenSharing) {
|
|
||||||
const started = await startScreenShare(room);
|
|
||||||
if (started) broadcastVoiceStatus();
|
|
||||||
} else {
|
|
||||||
await stopScreenShare(room);
|
|
||||||
broadcastVoiceStatus();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[VoiceControlBar] Failed to toggle screen share:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDisconnect = () => {
|
const handleDisconnect = () => handleDisconnectAction();
|
||||||
const { activeDmCall } = useVoiceStore.getState();
|
|
||||||
if (activeDmCall) {
|
|
||||||
wsSend({ type: 'dm_call_end', dmChannelId: activeDmCall.dmChannelId }, getChannelOrigin(activeDmCall.dmChannelId));
|
|
||||||
useVoiceStore.getState().setActiveDmCall(null);
|
|
||||||
} else {
|
|
||||||
wsSend({ type: 'voice_leave' }, voiceOrigin);
|
|
||||||
useVoiceStore.getState().leaveVoice();
|
|
||||||
}
|
|
||||||
if (voiceFullscreen) {
|
|
||||||
useUIStore.getState().setVoiceFullscreen(false);
|
|
||||||
if (document.fullscreenElement) {
|
|
||||||
document.exitFullscreen().catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFullscreen = () => {
|
const handleFullscreen = () => {
|
||||||
toggleVoiceFullscreen();
|
toggleVoiceFullscreen();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keyboard shortcuts
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
if (e.key === 'Escape' && voiceFullscreen) {
|
||||||
if (e.key === 'm' || e.key === 'M') {
|
|
||||||
e.preventDefault();
|
|
||||||
handleMute();
|
|
||||||
} else if (e.key === 'd' || e.key === 'D') {
|
|
||||||
e.preventDefault();
|
|
||||||
handleDeafen();
|
|
||||||
} else if (e.key === 'Escape' && voiceFullscreen) {
|
|
||||||
useUIStore.getState().setVoiceFullscreen(false);
|
useUIStore.getState().setVoiceFullscreen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [handleMute, handleDeafen, voiceFullscreen]);
|
}, [voiceFullscreen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-20 opacity-0 translate-y-4 group-hover/voice:opacity-100 group-hover/voice:translate-y-0 transition-all duration-300 ease-out">
|
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-20 opacity-0 translate-y-4 group-hover/voice:opacity-100 group-hover/voice:translate-y-0 transition-all duration-300 ease-out">
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { useVoiceStore } from '../stores/voiceStore';
|
||||||
|
import { useUIStore } from '../stores/uiStore';
|
||||||
|
import { getActiveRoom } from '../hooks/useLiveKit';
|
||||||
|
import { wsSend } from '../hooks/useWebSocket';
|
||||||
|
import { getChannelOrigin } from '../stores/spaceStore';
|
||||||
|
import { broadcastVoiceStatus, broadcastDeafenViaLiveKit } from './voice';
|
||||||
|
import { CAMERA_PRESET, startScreenShare, stopScreenShare } from './screenShare';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle mute. Respects space-mute/deafen guards.
|
||||||
|
* Extracted from VoiceControlBar so keybinds and buttons share the same logic.
|
||||||
|
*/
|
||||||
|
export function handleMuteAction(isSpaceMuted: boolean, isSpaceDeafened: boolean): void {
|
||||||
|
if (isSpaceMuted || isSpaceDeafened) return;
|
||||||
|
const wasDeafened = useVoiceStore.getState().isDeafened;
|
||||||
|
useVoiceStore.getState().toggleMic();
|
||||||
|
broadcastVoiceStatus();
|
||||||
|
if (wasDeafened && !useVoiceStore.getState().isDeafened) {
|
||||||
|
broadcastDeafenViaLiveKit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle deafen. Respects space-deafen guard.
|
||||||
|
*/
|
||||||
|
export function handleDeafenAction(isSpaceDeafened: boolean): void {
|
||||||
|
if (isSpaceDeafened) return;
|
||||||
|
useVoiceStore.getState().toggleDeafen();
|
||||||
|
broadcastVoiceStatus();
|
||||||
|
broadcastDeafenViaLiveKit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle camera. Requires LiveKit room.
|
||||||
|
*/
|
||||||
|
export async function handleCameraAction(): Promise<void> {
|
||||||
|
const room = getActiveRoom();
|
||||||
|
if (!room) return;
|
||||||
|
const isCameraOn = useVoiceStore.getState().isCameraOn;
|
||||||
|
try {
|
||||||
|
const willEnable = !isCameraOn;
|
||||||
|
if (willEnable) {
|
||||||
|
await room.localParticipant.setCameraEnabled(true,
|
||||||
|
{ resolution: CAMERA_PRESET.resolution },
|
||||||
|
{
|
||||||
|
videoCodec: CAMERA_PRESET.codec,
|
||||||
|
videoEncoding: CAMERA_PRESET.encoding,
|
||||||
|
simulcast: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await room.localParticipant.setCameraEnabled(false);
|
||||||
|
}
|
||||||
|
useVoiceStore.getState().toggleCamera();
|
||||||
|
broadcastVoiceStatus();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[voiceActions] Failed to toggle camera:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle screen share. Requires LiveKit room.
|
||||||
|
* Note: startScreenShare/stopScreenShare manage voiceStore.isScreenSharing internally.
|
||||||
|
* Do NOT call toggleScreenShare() here — it would double-flip the state.
|
||||||
|
*/
|
||||||
|
export async function handleScreenShareAction(): Promise<void> {
|
||||||
|
const room = getActiveRoom();
|
||||||
|
if (!room) return;
|
||||||
|
const isScreenSharing = useVoiceStore.getState().isScreenSharing;
|
||||||
|
try {
|
||||||
|
if (!isScreenSharing) {
|
||||||
|
const started = await startScreenShare(room);
|
||||||
|
if (started) broadcastVoiceStatus();
|
||||||
|
} else {
|
||||||
|
await stopScreenShare(room);
|
||||||
|
broadcastVoiceStatus();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[voiceActions] Failed to toggle screen share:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from voice. Handles DM call teardown and fullscreen exit.
|
||||||
|
*/
|
||||||
|
export function handleDisconnectAction(): void {
|
||||||
|
const voice = useVoiceStore.getState();
|
||||||
|
const { activeDmCall, currentVoiceChannelId } = voice;
|
||||||
|
|
||||||
|
if (activeDmCall) {
|
||||||
|
wsSend(
|
||||||
|
{ type: 'dm_call_end', dmChannelId: activeDmCall.dmChannelId },
|
||||||
|
getChannelOrigin(activeDmCall.dmChannelId)
|
||||||
|
);
|
||||||
|
voice.setActiveDmCall(null);
|
||||||
|
} else if (currentVoiceChannelId) {
|
||||||
|
const origin = getChannelOrigin(currentVoiceChannelId);
|
||||||
|
wsSend({ type: 'voice_leave' }, origin);
|
||||||
|
voice.leaveVoice();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit fullscreen if active
|
||||||
|
const voiceFullscreen = useUIStore.getState().voiceFullscreen;
|
||||||
|
if (voiceFullscreen) {
|
||||||
|
useUIStore.getState().setVoiceFullscreen(false);
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
document.exitFullscreen().catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user