feat(voice): jump to the call from the voice panel
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.
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useVoiceStore } from '../../stores/voiceStore';
|
import { useVoiceStore } from '../../stores/voiceStore';
|
||||||
|
import { useChatStore } from '../../stores/chatStore';
|
||||||
import { useSpaceStore, getChannelOrigin } from '../../stores/spaceStore';
|
import { useSpaceStore, getChannelOrigin } from '../../stores/spaceStore';
|
||||||
import { getActiveRoom } from '../../hooks/useLiveKit';
|
import { getActiveRoom } from '../../hooks/useLiveKit';
|
||||||
import { wsSend } from '../../hooks/useWebSocket';
|
import { wsSend } from '../../hooks/useWebSocket';
|
||||||
@@ -16,6 +18,10 @@ import { handleCameraAction } from '../../utils/voiceActions';
|
|||||||
*/
|
*/
|
||||||
export function VoiceControls() {
|
export function VoiceControls() {
|
||||||
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
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 isCameraOn = useVoiceStore((s) => s.isCameraOn);
|
||||||
const isScreenSharing = useVoiceStore((s) => s.isScreenSharing);
|
const isScreenSharing = useVoiceStore((s) => s.isScreenSharing);
|
||||||
const rnnoiseEnabled = useVoiceStore((s) => s.rnnoiseEnabled);
|
const rnnoiseEnabled = useVoiceStore((s) => s.rnnoiseEnabled);
|
||||||
@@ -42,7 +48,20 @@ export function VoiceControls() {
|
|||||||
if (!currentVoiceChannelId && !activeDmCall) return null;
|
if (!currentVoiceChannelId && !activeDmCall) return null;
|
||||||
|
|
||||||
const channel = channels.find(c => c.id === currentVoiceChannelId);
|
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 handleScreenShare = async () => {
|
||||||
const room = getActiveRoom();
|
const room = getActiveRoom();
|
||||||
@@ -120,9 +139,19 @@ export function VoiceControls() {
|
|||||||
<div className={`text-[13px] font-semibold leading-[18px] ${statusColor}`}>
|
<div className={`text-[13px] font-semibold leading-[18px] ${statusColor}`}>
|
||||||
{connectionError ? 'Connection Failed' : isLiveKitConnected ? 'Voice Connected' : 'Connecting...'}
|
{connectionError ? 'Connection Failed' : isLiveKitConnected ? 'Voice Connected' : 'Connecting...'}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[12px] text-txt-tertiary truncate leading-[16px]">
|
{canGoToCall ? (
|
||||||
{connectionError ? connectionError : channelName}
|
<button
|
||||||
</div>
|
onClick={handleGoToCall}
|
||||||
|
title="Go to call"
|
||||||
|
className="text-[12px] text-txt-tertiary truncate leading-[16px] w-full text-left hover:text-txt-primary hover:underline transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
{channelName}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="text-[12px] text-txt-tertiary truncate leading-[16px]">
|
||||||
|
{connectionError ? connectionError : channelName}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-0.5 flex-shrink-0">
|
<div className="flex items-center gap-0.5 flex-shrink-0">
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ export interface ScreenShareConfig {
|
|||||||
interface VoiceState {
|
interface VoiceState {
|
||||||
voiceUsers: Map<string, string[]>; // channelId → userIds
|
voiceUsers: Map<string, string[]>; // channelId → userIds
|
||||||
currentVoiceChannelId: string | null;
|
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;
|
isMuted: boolean;
|
||||||
isDeafened: boolean;
|
isDeafened: boolean;
|
||||||
isCameraOn: boolean;
|
isCameraOn: boolean;
|
||||||
@@ -85,7 +94,7 @@ interface VoiceState {
|
|||||||
setVoiceUsers: (channelId: string, userIds: string[]) => void;
|
setVoiceUsers: (channelId: string, userIds: string[]) => void;
|
||||||
addVoiceUser: (channelId: string, userId: string) => void;
|
addVoiceUser: (channelId: string, userId: string) => void;
|
||||||
removeVoiceUser: (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;
|
setParticipants: (participants: ParticipantInfo[]) => void;
|
||||||
setSpeakingParticipants: (ids: Set<string>) => void;
|
setSpeakingParticipants: (ids: Set<string>) => void;
|
||||||
setConnectionError: (error: string | null) => void;
|
setConnectionError: (error: string | null) => void;
|
||||||
@@ -158,6 +167,8 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
voiceUsers: new Map(),
|
voiceUsers: new Map(),
|
||||||
currentVoiceChannelId: null,
|
currentVoiceChannelId: null,
|
||||||
|
currentVoiceSpaceId: null,
|
||||||
|
currentVoiceChannelName: null,
|
||||||
isMuted: false,
|
isMuted: false,
|
||||||
pttActive: false,
|
pttActive: false,
|
||||||
isDeafened: false,
|
isDeafened: false,
|
||||||
@@ -349,8 +360,10 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setCurrentVoiceChannel: (channelId) => set({
|
setCurrentVoiceChannel: (channelId, spaceId = null, channelName = null) => set({
|
||||||
currentVoiceChannelId: channelId,
|
currentVoiceChannelId: channelId,
|
||||||
|
currentVoiceSpaceId: channelId ? spaceId : null,
|
||||||
|
currentVoiceChannelName: channelId ? channelName : null,
|
||||||
activeDmCall: null // Clear active DM call when joining a server channel
|
activeDmCall: null // Clear active DM call when joining a server channel
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -516,6 +529,8 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
voiceUsers: new Map(),
|
voiceUsers: new Map(),
|
||||||
voiceUserStates: new Map(),
|
voiceUserStates: new Map(),
|
||||||
currentVoiceChannelId: null,
|
currentVoiceChannelId: null,
|
||||||
|
currentVoiceSpaceId: null,
|
||||||
|
currentVoiceChannelName: null,
|
||||||
participants: [],
|
participants: [],
|
||||||
speakingParticipantIds: new Set(),
|
speakingParticipantIds: new Set(),
|
||||||
speakingUserIds: new Set(),
|
speakingUserIds: new Set(),
|
||||||
|
|||||||
@@ -152,7 +152,15 @@ export function joinVoiceChannel(
|
|||||||
if (myOldId) removeVoiceUser(currentVoiceChannelId, myOldId);
|
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)
|
// Optimistic: immediately show self in new channel (using origin-aware ID)
|
||||||
const myNewId = getMyUserIdForOrigin(getChannelOrigin(channelId));
|
const myNewId = getMyUserIdForOrigin(getChannelOrigin(channelId));
|
||||||
if (myNewId) addVoiceUser(channelId, myNewId);
|
if (myNewId) addVoiceUser(channelId, myNewId);
|
||||||
|
|||||||
Reference in New Issue
Block a user