fix: add invite button, voice join dedup, voice panel immediate render

This commit is contained in:
2026-06-29 15:28:05 -04:00
parent a8a09cf7b4
commit 2d3bf48e66
3 changed files with 52 additions and 12 deletions
+14
View File
@@ -3,6 +3,7 @@ import { useServerStore } from '../stores/server.ts';
import { useChannelStore } from '../stores/channel.ts'; import { useChannelStore } from '../stores/channel.ts';
import { VoiceChannel } from './VoiceChannel.tsx'; import { VoiceChannel } from './VoiceChannel.tsx';
import { CreateChannelModal } from './CreateChannelModal.tsx'; import { CreateChannelModal } from './CreateChannelModal.tsx';
import { InviteModal } from './InviteModal.tsx';
export function ChannelList() { export function ChannelList() {
const activeServerId = useServerStore((state) => state.activeServerId); const activeServerId = useServerStore((state) => state.activeServerId);
@@ -12,6 +13,7 @@ export function ChannelList() {
const activeChannelId = useChannelStore((state) => state.activeChannelId); const activeChannelId = useChannelStore((state) => state.activeChannelId);
const setActiveChannel = useChannelStore((state) => state.setActiveChannel); const setActiveChannel = useChannelStore((state) => state.setActiveChannel);
const [showCreate, setShowCreate] = useState(false); const [showCreate, setShowCreate] = useState(false);
const [showInvite, setShowInvite] = useState(false);
useEffect(() => { useEffect(() => {
if (activeServerId) { if (activeServerId) {
@@ -47,6 +49,14 @@ export function ChannelList() {
<div className="terminal-border border-t-0 border-x-0 px-3 py-2 text-gb-fg truncate flex items-center justify-between"> <div className="terminal-border border-t-0 border-x-0 px-3 py-2 text-gb-fg truncate flex items-center justify-between">
<span>{activeServerId ? `[SERVER ${activeServer?.name ?? activeServerId}]` : '[NO SERVER]'}</span> <span>{activeServerId ? `[SERVER ${activeServer?.name ?? activeServerId}]` : '[NO SERVER]'}</span>
{activeServerId && ( {activeServerId && (
<div className="flex gap-1">
<button
onClick={() => setShowInvite(true)}
className="terminal-button text-xs"
title="Invite people"
>
[INV]
</button>
<button <button
onClick={() => setShowCreate(true)} onClick={() => setShowCreate(true)}
className="terminal-button text-xs" className="terminal-button text-xs"
@@ -54,6 +64,7 @@ export function ChannelList() {
> >
[+] [+]
</button> </button>
</div>
)} )}
</div> </div>
<div className="flex-1 overflow-y-auto p-2 font-mono text-sm"> <div className="flex-1 overflow-y-auto p-2 font-mono text-sm">
@@ -90,6 +101,9 @@ export function ChannelList() {
{showCreate && activeServerId && ( {showCreate && activeServerId && (
<CreateChannelModal serverId={activeServerId} onClose={() => setShowCreate(false)} /> <CreateChannelModal serverId={activeServerId} onClose={() => setShowCreate(false)} />
)} )}
{showInvite && activeServerId && activeServer && (
<InviteModal serverId={activeServerId} serverName={activeServer.name} onClose={() => setShowInvite(false)} />
)}
</div> </div>
); );
} }
+9 -1
View File
@@ -15,12 +15,20 @@ export function VoiceChannel({ channelId, channelName }: VoiceChannelProps) {
const handleClick = () => { const handleClick = () => {
if (!isActive) { if (!isActive) {
joinVoice(channelId, channelName); 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 ( return (
<div className="mb-0.5"> <div className="mb-0.5">
{error && (
<div className="px-2 py-0.5 text-gb-red text-xxs">! {error}</div>
)}
<button <button
onClick={handleClick} onClick={handleClick}
className={`w-full text-left px-2 py-1 rounded-sm flex items-center gap-2 ${ className={`w-full text-left px-2 py-1 rounded-sm flex items-center gap-2 ${
+22 -4
View File
@@ -35,6 +35,7 @@ interface VoiceState {
participants: VoiceParticipant[]; participants: VoiceParticipant[];
error: string | null; error: string | null;
_room: Room | null; _room: Room | null;
isJoining: boolean;
joinVoice: (channelId: string, channelName: string) => Promise<void>; joinVoice: (channelId: string, channelName: string) => Promise<void>;
leaveVoice: () => Promise<void>; leaveVoice: () => Promise<void>;
toggleMute: () => void; toggleMute: () => void;
@@ -64,8 +65,15 @@ export const useVoiceStore = create<VoiceState>((set, get) => ({
participants: [], participants: [],
error: null, error: null,
_room: null, _room: null,
isJoining: false,
joinVoice: async (channelId, channelName) => { joinVoice: async (channelId, channelName) => {
if (get().isJoining) {
console.log("[voice] already joining, ignoring duplicate click");
return;
}
set({ isJoining: true });
console.log("[voice] joinVoice called:", channelId);
const existing = get()._room; const existing = get()._room;
if (existing) { if (existing) {
await get().leaveVoice(); await get().leaveVoice();
@@ -74,9 +82,11 @@ export const useVoiceStore = create<VoiceState>((set, get) => ({
set({ error: null }); set({ error: null });
try { try {
console.log("[voice] requesting token for:", channelId);
const resp = await api.post<VoiceTokenResponse>('/voice/token', { const resp = await api.post<VoiceTokenResponse>('/voice/token', {
room_name: channelId, room_name: channelId,
}); });
console.log("[voice] got token response:", resp);
const room = new Room({ const room = new Room({
adaptiveStream: true, adaptiveStream: true,
@@ -153,16 +163,24 @@ export const useVoiceStore = create<VoiceState>((set, get) => ({
await room.connect(resp.livekit_url, resp.token); await room.connect(resp.livekit_url, resp.token);
// Mute mic by default on join // Set state immediately so the UI renders the voice panel
await room.localParticipant.setMicrophoneEnabled(false);
set({ isMuted: true });
set({ set({
_room: room, _room: room,
currentRoom: channelId, currentRoom: channelId,
currentRoomName: channelName, currentRoomName: channelName,
error: null,
}); });
// Mute mic by default on join (best effort; don't fail join if this errors)
try {
await room.localParticipant.setMicrophoneEnabled(false);
set({ isMuted: true });
} catch (micErr) {
console.warn("[voice] failed to set initial mute state:", micErr);
}
set({ isJoining: false });
// Notify WebSocket peers // Notify WebSocket peers
const wsSend = useWebSocketStore.getState().send; const wsSend = useWebSocketStore.getState().send;
wsSend({ wsSend({