Files
dumpsterChat/web/src/components/VoiceChannel.tsx
T

122 lines
4.3 KiB
TypeScript

import { useVoiceStore } from '../stores/voice.ts';
import { useAuthStore } from '../stores/auth.ts';
import { usePermissions } from '../lib/usePermissions.ts';
import { useChannelStore } from '../stores/channel.ts';
import { api } from '../lib/api.ts';
interface VoiceChannelProps {
channelId: string;
channelName: string;
}
export function VoiceChannel({ channelId, channelName }: VoiceChannelProps) {
const currentRoom = useVoiceStore((state) => state.currentRoom);
const isConnected = useVoiceStore((state) => state.isConnected);
const joinVoice = useVoiceStore((state) => state.joinVoice);
const participants = useVoiceStore((state) => state.participants);
const currentUserId = useAuthStore((state) => state.user?.id);
const activeChannelId = useChannelStore((s) => s.activeChannelId);
const channelsByServer = useChannelStore((s) => s.channelsByServer);
const serverId = activeChannelId
? Object.entries(channelsByServer).find(([_, channels]) => channels.some(c => c.id === activeChannelId))?.[0]
: null;
const { canMute } = usePermissions(serverId || null);
const handleMute = async (identity: string) => {
if (!currentRoom) return;
try {
await api.post(`/voice/rooms/${currentRoom}/participants/${identity}/mute`);
} catch (err) {
console.error('Failed to mute participant:', err);
}
};
const whisperTo = (identity: string, username: string) => {
useVoiceStore.getState().whisperTo(identity, username);
};
const isActive = currentRoom === channelId;
const handleClick = () => {
if (!isActive) {
console.log("[VoiceChannel] joining voice:", channelId, channelName);
joinVoice(channelId, channelName).catch((err) => {
console.error("[VoiceChannel] joinVoice failed:", err);
});
}
};
const error = useVoiceStore((state) => state.error);
return (
<div className="mb-0.5">
{error && (
<div className="px-2 py-0.5 text-gb-red text-xxs">! {error}</div>
)}
<button
onClick={handleClick}
className={`w-full text-left px-2 py-1 rounded-sm flex items-center gap-2 ${
isActive ? 'terminal-active' : 'hover:bg-gb-bg-t text-gb-fg-s'
}`}
>
<span className={isActive ? 'text-gb-orange' : 'text-gb-fg-f'}>
{isActive && isConnected ? '●' : isActive ? '◐' : '○'}
</span>
<span className={isActive ? 'text-gb-orange' : 'text-gb-fg-f'}>
🔊
</span>
<span className="truncate">{channelName}</span>
{isActive && participants.length > 0 && (
<span className="ml-auto text-xxs text-gb-fg-f">
[{participants.length}]
</span>
)}
</button>
{isActive && (
<div className="pl-7 py-0.5">
{participants.map((p) => {
const isMe = p.identity === currentUserId;
return (
<div
key={p.identity}
className="text-xxs text-gb-fg-f flex items-center gap-1 group"
>
<span className={p.isSpeaking ? 'text-gb-green' : 'text-gb-fg-f'}>
{p.isMuted ? '○' : '●'}
</span>
<span className={p.isSpeaking ? 'text-gb-green' : ''}>
{p.username}
</span>
{!isMe && (
<div className="ml-auto opacity-0 group-hover:opacity-100 flex items-center gap-1">
{canMute && !p.isMuted && (
<button
className="text-gb-fg-f hover:text-gb-red"
onClick={(e) => { e.stopPropagation(); handleMute(p.identity); }}
title={`Mute ${p.username}`}
>
[mute]
</button>
)}
<button
className="text-gb-fg-f hover:text-gb-orange"
onClick={(e) => { e.stopPropagation(); whisperTo(p.identity, p.username); }}
title={`Whisper to ${p.username}`}
>
[whisper]
</button>
</div>
)}
</div>
);
})}
</div>
)}
</div>
);
}