fix: add invite button, voice join dedup, voice panel immediate render
This commit is contained in:
@@ -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,13 +49,22 @@ 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 && (
|
||||||
<button
|
<div className="flex gap-1">
|
||||||
onClick={() => setShowCreate(true)}
|
<button
|
||||||
className="terminal-button text-xs"
|
onClick={() => setShowInvite(true)}
|
||||||
title="Create channel"
|
className="terminal-button text-xs"
|
||||||
>
|
title="Invite people"
|
||||||
[+]
|
>
|
||||||
</button>
|
[INV]
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreate(true)}
|
||||||
|
className="terminal-button text-xs"
|
||||||
|
title="Create channel"
|
||||||
|
>
|
||||||
|
[+]
|
||||||
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user