From c70b0095a99b020dee162ea8fca781d398663c8b Mon Sep 17 00:00:00 2001 From: devsyncwrld Date: Mon, 31 Aug 2026 11:40:45 -0300 Subject: [PATCH] feat(voice): jump to the call from the voice panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The channel name under 'Voice Connected' was a plain div. Making it navigate needed more than an onClick: voiceStore never recorded which space the call was in, and spaceStore.channels only holds the space currently being viewed — so after navigating away the call's channel was unresolvable, which is also why the label degraded to a generic 'Voice Channel'. Capture space and channel name at join time (the only moment they are reliable) and use them for both the label and the jump. Covers space calls and DM calls. --- .../src/components/voice/VoiceControls.tsx | 37 +++++++++++++++++-- packages/web/src/stores/voiceStore.ts | 19 +++++++++- packages/web/src/utils/voice.ts | 10 ++++- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/packages/web/src/components/voice/VoiceControls.tsx b/packages/web/src/components/voice/VoiceControls.tsx index c0e45c60..c31494fd 100644 --- a/packages/web/src/components/voice/VoiceControls.tsx +++ b/packages/web/src/components/voice/VoiceControls.tsx @@ -1,5 +1,7 @@ import React, { useState, useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; import { useVoiceStore } from '../../stores/voiceStore'; +import { useChatStore } from '../../stores/chatStore'; import { useSpaceStore, getChannelOrigin } from '../../stores/spaceStore'; import { getActiveRoom } from '../../hooks/useLiveKit'; import { wsSend } from '../../hooks/useWebSocket'; @@ -16,6 +18,10 @@ import { handleCameraAction } from '../../utils/voiceActions'; */ export function VoiceControls() { const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId); + const currentVoiceSpaceId = useVoiceStore((s) => s.currentVoiceSpaceId); + const currentVoiceChannelName = useVoiceStore((s) => s.currentVoiceChannelName); + const setCurrentChannel = useChatStore((s) => s.setCurrentChannel); + const navigate = useNavigate(); const isCameraOn = useVoiceStore((s) => s.isCameraOn); const isScreenSharing = useVoiceStore((s) => s.isScreenSharing); const rnnoiseEnabled = useVoiceStore((s) => s.rnnoiseEnabled); @@ -42,7 +48,20 @@ export function VoiceControls() { if (!currentVoiceChannelId && !activeDmCall) return null; const channel = channels.find(c => c.id === currentVoiceChannelId); - const channelName = channel?.name ?? (activeDmCall ? 'DM Call' : 'Voice Channel'); + const channelName = + channel?.name ?? currentVoiceChannelName ?? (activeDmCall ? 'DM Call' : 'Voice Channel'); + + // Jump back to where the call is happening. DM calls live under @me; space + // calls under the space captured at join time. + const callSpaceId = activeDmCall ? '@me' : currentVoiceSpaceId; + const callChannelId = activeDmCall ? activeDmCall.dmChannelId : currentVoiceChannelId; + const canGoToCall = Boolean(callChannelId && callSpaceId && !connectionError); + + const handleGoToCall = () => { + if (!canGoToCall || !callChannelId) return; + setCurrentChannel(callChannelId); + navigate(`/channels/${callSpaceId}/${callChannelId}`); + }; const handleScreenShare = async () => { const room = getActiveRoom(); @@ -120,9 +139,19 @@ export function VoiceControls() {
{connectionError ? 'Connection Failed' : isLiveKitConnected ? 'Voice Connected' : 'Connecting...'}
-
- {connectionError ? connectionError : channelName} -
+ {canGoToCall ? ( + + ) : ( +
+ {connectionError ? connectionError : channelName} +
+ )}
diff --git a/packages/web/src/stores/voiceStore.ts b/packages/web/src/stores/voiceStore.ts index 82b486db..6e54b7d1 100644 --- a/packages/web/src/stores/voiceStore.ts +++ b/packages/web/src/stores/voiceStore.ts @@ -17,6 +17,15 @@ export interface ScreenShareConfig { interface VoiceState { voiceUsers: Map; // channelId → userIds currentVoiceChannelId: string | null; + /** + * Space and name of the channel the call is in, captured at join time. + * `channels` in spaceStore only holds the space the user is *viewing*, so + * once they navigate elsewhere the call's channel is no longer resolvable + * from it — these keep the voice panel able to name and link to the call. + * Transient: intentionally absent from `partialize`. + */ + currentVoiceSpaceId: string | null; + currentVoiceChannelName: string | null; isMuted: boolean; isDeafened: boolean; isCameraOn: boolean; @@ -85,7 +94,7 @@ interface VoiceState { setVoiceUsers: (channelId: string, userIds: string[]) => void; addVoiceUser: (channelId: string, userId: string) => void; removeVoiceUser: (channelId: string, userId: string) => void; - setCurrentVoiceChannel: (channelId: string | null) => void; + setCurrentVoiceChannel: (channelId: string | null, spaceId?: string | null, channelName?: string | null) => void; setParticipants: (participants: ParticipantInfo[]) => void; setSpeakingParticipants: (ids: Set) => void; setConnectionError: (error: string | null) => void; @@ -158,6 +167,8 @@ export const useVoiceStore = create()( (set, get) => ({ voiceUsers: new Map(), currentVoiceChannelId: null, + currentVoiceSpaceId: null, + currentVoiceChannelName: null, isMuted: false, pttActive: false, isDeafened: false, @@ -349,8 +360,10 @@ export const useVoiceStore = create()( }); }, - setCurrentVoiceChannel: (channelId) => set({ + setCurrentVoiceChannel: (channelId, spaceId = null, channelName = null) => set({ currentVoiceChannelId: channelId, + currentVoiceSpaceId: channelId ? spaceId : null, + currentVoiceChannelName: channelId ? channelName : null, activeDmCall: null // Clear active DM call when joining a server channel }), @@ -516,6 +529,8 @@ export const useVoiceStore = create()( voiceUsers: new Map(), voiceUserStates: new Map(), currentVoiceChannelId: null, + currentVoiceSpaceId: null, + currentVoiceChannelName: null, participants: [], speakingParticipantIds: new Set(), speakingUserIds: new Set(), diff --git a/packages/web/src/utils/voice.ts b/packages/web/src/utils/voice.ts index 65fd9892..b6bb5873 100644 --- a/packages/web/src/utils/voice.ts +++ b/packages/web/src/utils/voice.ts @@ -152,7 +152,15 @@ export function joinVoiceChannel( if (myOldId) removeVoiceUser(currentVoiceChannelId, myOldId); } - setCurrentVoiceChannel(channelId); + // Capture the space and name now: a voice channel can only be joined from + // within its own space, but the user may navigate away afterwards — at which + // point spaceStore.channels no longer resolves this channel. + const spaceStore = useSpaceStore.getState(); + setCurrentVoiceChannel( + channelId, + spaceStore.currentSpaceId ?? null, + spaceStore.channels.find((c) => c.id === channelId)?.name ?? null, + ); // Optimistic: immediately show self in new channel (using origin-aware ID) const myNewId = getMyUserIdForOrigin(getChannelOrigin(channelId)); if (myNewId) addVoiceUser(channelId, myNewId);