Proper vc leaving
This commit is contained in:
@@ -180,21 +180,22 @@ pub fn list_output_devices() -> Result<Vec<String>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// CONFIG UPDATES & DISCONNECT
|
// DISCONNECT
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn disconnect_from_vc(voice_state: State<VoiceState>) -> Result<(), String> {
|
pub fn disconnect_from_vc(voice_state: State<VoiceState>) -> Result<(), String> {
|
||||||
if let Some(session) = voice_state.session.lock().unwrap().take() {
|
let mut lock = voice_state.session.lock().map_err(|e| e.to_string())?;
|
||||||
// Signal shutdown atomic to terminate sender and receiver loops
|
|
||||||
|
if let Some(session) = lock.take() {
|
||||||
session.shutdown.store(true, Ordering::SeqCst);
|
session.shutdown.store(true, Ordering::SeqCst);
|
||||||
|
|
||||||
// Pause CPAL hardware streams explicitly to flush playback drivers immediately
|
|
||||||
let _ = session.input_stream.pause();
|
let _ = session.input_stream.pause();
|
||||||
if let Ok(output) = session.output_stream.lock() {
|
if let Ok(output) = session.output_stream.lock() {
|
||||||
let _ = output.pause();
|
let _ = output.pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,3 +492,17 @@ fn build_output_stream(
|
|||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for VoiceSession {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.shutdown.store(true, Ordering::SeqCst);
|
||||||
|
|
||||||
|
let _ = self.input_stream.pause();
|
||||||
|
if let Ok(output) = self.output_stream.lock() {
|
||||||
|
let _ = output.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output hardware stream handle clean-up
|
||||||
|
eprintln!("[vc] VoiceSession dropped and audio streams paused.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -219,6 +219,17 @@ export default class Enclave<P = Page> {
|
|||||||
this.server?.getUsers([msg.pubkey]);
|
this.server?.getUsers([msg.pubkey]);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
case "UserLeftVoice":
|
||||||
|
if (!server.voiceChatUsers[msg.channel_id]) return;
|
||||||
|
|
||||||
|
server.voiceChatUsers[msg.channel_id] = server.voiceChatUsers[
|
||||||
|
msg.channel_id
|
||||||
|
].filter((v) => v !== msg.pubkey);
|
||||||
|
|
||||||
|
this.forceRender();
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
case "Speaking":
|
case "Speaking":
|
||||||
clearTimeout(server.voiceChatSpeakers[msg.pubkey]);
|
clearTimeout(server.voiceChatSpeakers[msg.pubkey]);
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ export type ClientMethod =
|
|||||||
channel_id: string;
|
channel_id: string;
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
method: "UserLeftVoice";
|
||||||
|
channel_id: string;
|
||||||
|
pubkey: string;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
method: "Speaking";
|
method: "Speaking";
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
@@ -89,4 +94,7 @@ export type ServerMethod =
|
|||||||
| {
|
| {
|
||||||
method: "JoinVoice";
|
method: "JoinVoice";
|
||||||
channel_id: string;
|
channel_id: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
method: "LeaveVoice";
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ export default class EnclaveServer {
|
|||||||
public messages: Record<string, Record<string, StoredMessage>>;
|
public messages: Record<string, Record<string, StoredMessage>>;
|
||||||
public users: Record<string, ClientMeta | null>;
|
public users: Record<string, ClientMeta | null>;
|
||||||
|
|
||||||
public voiceJoin?: (pin: number, channelId: string) => void;
|
public voiceJoin?: (pin: bigint, channelId: string) => void;
|
||||||
public voiceChatUsers: Record<string, string[]>;
|
public voiceChatUsers: Record<string, string[]>;
|
||||||
public voiceChatSpeakers: Record<string, NodeJS.Timeout>;
|
public voiceChatSpeakers: Record<string, NodeJS.Timeout>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import Enclave from "@/app/app";
|
import Enclave from "@/app/app";
|
||||||
import { ChannelPageProps } from "../PageView";
|
import { ChannelPageProps } from "../PageView";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Phone, PhoneOff } from "lucide-react";
|
||||||
|
|
||||||
export default function VoiceChannel({
|
export default function VoiceChannel({
|
||||||
appRef,
|
appRef,
|
||||||
@@ -13,28 +15,49 @@ export default function VoiceChannel({
|
|||||||
const channel = appRef.current?.page?.channel;
|
const channel = appRef.current?.page?.channel;
|
||||||
if (!channel) return null;
|
if (!channel) return null;
|
||||||
|
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const lastChannelId = useRef<string | null>(null);
|
const lastChannelId = useRef<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
// Leave current channel setup
|
||||||
|
const handleLeave = () => {
|
||||||
|
appRef.current?.server?.websocket?.send({ method: "LeaveVoice" });
|
||||||
|
invoke("disconnect_from_vc").catch(console.error);
|
||||||
|
setIsConnected(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Explicit action to join the voice channel
|
||||||
|
const handleJoin = () => {
|
||||||
const hostname = appRef.current?.server?.hostname;
|
const hostname = appRef.current?.server?.hostname;
|
||||||
if (!hostname) return;
|
if (!hostname || channel.kind !== "voice" || !appRef.current?.server)
|
||||||
if (lastChannelId.current === channel.id) return;
|
return;
|
||||||
lastChannelId.current = channel.id;
|
|
||||||
|
|
||||||
invoke("disconnect_from_vc");
|
|
||||||
|
|
||||||
if (channel.kind !== "voice" || !appRef.current?.server) return;
|
|
||||||
|
|
||||||
appRef.current.server.voiceJoin = (pin, channelId) => {
|
appRef.current.server.voiceJoin = (pin, channelId) => {
|
||||||
invoke("connect_to_vc", { hostname, pin, channelId }).catch(
|
invoke("connect_to_vc", { hostname, pin, channelId })
|
||||||
console.error,
|
.then(() => setIsConnected(true))
|
||||||
);
|
.catch(console.error);
|
||||||
};
|
};
|
||||||
|
|
||||||
appRef.current.server.websocket?.send({
|
appRef.current.server.websocket?.send({
|
||||||
method: "JoinVoice",
|
method: "JoinVoice",
|
||||||
channel_id: channel.id,
|
channel_id: channel.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setIsConnected(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clean up when channel changes or component unmounts
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastChannelId.current !== channel.id) {
|
||||||
|
if (isConnected) {
|
||||||
|
invoke("disconnect_from_vc").catch(console.error);
|
||||||
|
setIsConnected(false);
|
||||||
|
}
|
||||||
|
lastChannelId.current = channel.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
invoke("disconnect_from_vc").catch(console.error);
|
||||||
|
};
|
||||||
}, [channel.id]);
|
}, [channel.id]);
|
||||||
|
|
||||||
const speakers =
|
const speakers =
|
||||||
@@ -42,7 +65,6 @@ export default function VoiceChannel({
|
|||||||
Object.keys(appRef.current?.server?.voiceChatSpeakers);
|
Object.keys(appRef.current?.server?.voiceChatSpeakers);
|
||||||
|
|
||||||
const users = appRef.current?.server?.voiceChatUsers[channel.id];
|
const users = appRef.current?.server?.voiceChatUsers[channel.id];
|
||||||
|
|
||||||
const usersLength = users?.length || 0;
|
const usersLength = users?.length || 0;
|
||||||
|
|
||||||
// Dynamic grid column layout based on user count
|
// Dynamic grid column layout based on user count
|
||||||
@@ -61,13 +83,19 @@ export default function VoiceChannel({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen gap-2">
|
<div className="flex flex-col h-screen gap-2 pb-3">
|
||||||
<header className="px-3 pt-3 pb-3 text-sm text-muted-foreground border-b border-b-border">
|
{/* Header */}
|
||||||
|
<header className="px-3 pt-3 pb-3 text-sm text-muted-foreground border-b border-b-border flex justify-between items-center">
|
||||||
<h2>{channel.name}</h2>
|
<h2>{channel.name}</h2>
|
||||||
|
<span className="text-xs">
|
||||||
|
{usersLength} {usersLength === 1 ? "Participant" : "Participants"}
|
||||||
|
</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{/* Main Grid */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid p-4 w-full gap-4 h-full auto-rows-fr bg-background/50 rounded-xl overflow-y-auto",
|
"grid p-4 w-full gap-4 flex-1 auto-rows-fr bg-background/50 rounded-xl overflow-y-auto",
|
||||||
getGridCols(),
|
getGridCols(),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -101,15 +129,9 @@ export default function VoiceChannel({
|
|||||||
{fallbackLetter}
|
{fallbackLetter}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|
||||||
{/* Mute indicator badge overlay */}
|
|
||||||
{/*{user?.isMuted && (
|
|
||||||
<div className="absolute -bottom-1 -right-1 p-1.5 bg-destructive rounded-full text-destructive-foreground shadow-md">
|
|
||||||
<MicOff className="w-3.5 h-3.5" />
|
|
||||||
</div>
|
|
||||||
)}*/}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Name Plate */}
|
||||||
<div className="absolute bottom-3 left-3 max-w-[85%] truncate bg-background/80 backdrop-blur-md text-foreground py-1 px-2.5 rounded-md border border-border/50 text-xs font-medium shadow-sm">
|
<div className="absolute bottom-3 left-3 max-w-[85%] truncate bg-background/80 backdrop-blur-md text-foreground py-1 px-2.5 rounded-md border border-border/50 text-xs font-medium shadow-sm">
|
||||||
{displayName}
|
{displayName}
|
||||||
</div>
|
</div>
|
||||||
@@ -117,6 +139,29 @@ export default function VoiceChannel({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Control Bar */}
|
||||||
|
<footer className="flex items-center justify-center px-4 py-2 border-t border-border/40 bg-card/50 rounded-xl mx-2">
|
||||||
|
{isConnected ? (
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleLeave}
|
||||||
|
className="flex items-center gap-2 font-medium px-6"
|
||||||
|
>
|
||||||
|
<PhoneOff className="w-4 h-4" />
|
||||||
|
Disconnect
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={handleJoin}
|
||||||
|
className="flex items-center gap-2 font-medium px-6 bg-emerald-600 hover:bg-emerald-700 text-white"
|
||||||
|
>
|
||||||
|
<Phone className="w-4 h-4" />
|
||||||
|
Join Voice
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user