diff --git a/src/app/app.ts b/src/app/app.ts index 5ca3c46..a5abb25 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -170,6 +170,8 @@ export default class Enclave

{ if (!server || channel.kind !== "voice") return; + if (server.voiceChannelId) this.leaveVoice(); + server.voiceJoin = (pin, channelId) => { invoke("connect_to_vc", { hostname: server.hostname, @@ -177,14 +179,12 @@ export default class Enclave

{ sharedSecret: server.websocket?.sharedSecret, }) .then(() => { - server.isInVoiceChat = true; server.voiceChannelId = channelId; this.forceRender(); }) .catch(console.error); }; - server.isInVoiceChat = true; server.voiceChannelId = channel.id; server.websocket?.send({ method: "JoinVoice", channel_id: channel.id }); @@ -195,12 +195,11 @@ export default class Enclave

{ public leaveVoice() { const server = this.server; - if (!server || !server.isInVoiceChat) return; + if (!server || !server.voiceChannelId) return; server.websocket?.send({ method: "LeaveVoice" }); invoke("disconnect_from_vc").catch(console.error); - server.isInVoiceChat = false; server.voiceChannelId = undefined; server.voiceChatSpeakers = {}; diff --git a/src/app/server.ts b/src/app/server.ts index ca67ec3..4b27b4c 100644 --- a/src/app/server.ts +++ b/src/app/server.ts @@ -37,7 +37,6 @@ export default class EnclaveServer { public voiceJoin?: (pin: bigint, channelId: string) => void; public voiceChatUsers: Record; public voiceChatSpeakers: Record; - public isInVoiceChat: boolean; public voiceChannelId?: string; public constructor(hostname: string, isSecure: boolean) { @@ -47,7 +46,6 @@ export default class EnclaveServer { this.users = {}; this.voiceChatUsers = {}; this.voiceChatSpeakers = {}; - this.isInVoiceChat = false; } public disconnect() { diff --git a/src/app/ws.ts b/src/app/ws.ts index f3f2f9c..5e3e3b0 100644 --- a/src/app/ws.ts +++ b/src/app/ws.ts @@ -17,7 +17,6 @@ export default class EnclaveWebSocket { onOpenQueue: Array<() => void>; private sendCounter = 0n; - private recvCounter = 0n; public sharedSecret: Uint8Array | null = null; private readonly handshakeReady: Promise; @@ -78,14 +77,6 @@ export default class EnclaveWebSocket { return nonce; } - private nextRecvNonce(): Uint8Array { - const nonce = new Uint8Array(12); - const view = new DataView(nonce.buffer); - view.setBigUint64(0, this.recvCounter, false); - this.recvCounter += 1n; - return nonce; - } - private encrypt(plaintext: Uint8Array): Uint8Array { if (!this.sharedSecret) throw new Error("Handshake not complete"); diff --git a/src/components/page/channel/VoiceChannel.tsx b/src/components/page/channel/VoiceChannel.tsx index 2fe0fc1..9ff1d54 100644 --- a/src/components/page/channel/VoiceChannel.tsx +++ b/src/components/page/channel/VoiceChannel.tsx @@ -1,8 +1,9 @@ import Enclave from "@/app/app"; import { ChannelPageProps } from "../PageView"; -import { useEffect } from "react"; import { cn } from "@/lib/utils"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { Phone, Volume2 } from "lucide-react"; export default function VoiceChannel({ appRef, @@ -11,23 +12,15 @@ export default function VoiceChannel({ }) { const channel = appRef.current?.page?.channel; - useEffect(() => { - if (!channel || channel.kind !== "voice") return; - - const server = appRef.current?.server; - - if (!server || server.voiceChannelId === channel.id) return; - - appRef.current?.joinVoice(channel); - }, [channel?.id]); - if (!channel) return null; - const speakers = - appRef.current?.server?.voiceChatSpeakers && - Object.keys(appRef.current?.server?.voiceChatSpeakers); + const server = appRef.current?.server; + const isConnected = server?.voiceChannelId === channel.id; - const users = appRef.current?.server?.voiceChatUsers[channel.id]; + const speakers = + server?.voiceChatSpeakers && Object.keys(server.voiceChatSpeakers); + + const users = server?.voiceChatUsers[channel.id]; const usersLength = users?.length || 0; // Dynamic grid column layout based on user count @@ -45,63 +38,104 @@ export default function VoiceChannel({ return "h-14 w-14 text-lg"; }; + const handleJoinVoice = () => { + appRef.current?.joinVoice(channel); + }; + return ( -

+
{/* Header */} -
+

{channel.name}

{usersLength} {usersLength === 1 ? "Participant" : "Participants"}
- {/* Main Grid */} -
- {users?.map((pubkey) => { - const user = appRef.current?.server?.users[pubkey]; - const isSpeaking = speakers?.includes(pubkey); - const displayName = user?.displayName || "Unknown User"; - const fallbackLetter = displayName.charAt(0).toUpperCase(); + {/* Main Grid Container */} +
+ {/* Active Grid - Fully readable at all times */} +
+ {users?.map((pubkey) => { + const user = server?.users[pubkey]; + const isSpeaking = speakers?.includes(pubkey); + const displayName = user?.displayName || "Unknown User"; + const fallbackLetter = displayName.charAt(0).toUpperCase(); - return ( -
- {/* Avatar Container */} -
- - - - {fallbackLetter} - - + return ( +
+ {/* Avatar Container */} +
+ + + + {fallbackLetter} + + +
+ + {/* Name Plate */} +
+ {displayName} +
+
+ ); + })} +
+ + {/* Floating Join CTA Overlay */} + {!isConnected && ( +
+
+
+
- {/* Name Plate */} -
- {displayName} +
+

+ Ready to join {channel.name}? +

+

+ {usersLength > 0 + ? `${usersLength} ${usersLength === 1 ? "participant is" : "participants are"} currently connected.` + : "Channel is currently empty."} +

+ +
- ); - })} +
+ )}
); -} \ No newline at end of file +} diff --git a/src/components/view/StatusCard.tsx b/src/components/view/StatusCard.tsx index faee4fb..4644de8 100644 --- a/src/components/view/StatusCard.tsx +++ b/src/components/view/StatusCard.tsx @@ -39,7 +39,7 @@ export default function StatusCard({ return (
- {server?.isInVoiceChat && voiceChannel && ( + {voiceChannel && (