Better voice chat UI and fix bug
This commit is contained in:
+3
-4
@@ -170,6 +170,8 @@ export default class Enclave<P = Page> {
|
||||
|
||||
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<P = Page> {
|
||||
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<P = Page> {
|
||||
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 = {};
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ export default class EnclaveServer {
|
||||
public voiceJoin?: (pin: bigint, channelId: string) => void;
|
||||
public voiceChatUsers: Record<string, string[]>;
|
||||
public voiceChatSpeakers: Record<string, NodeJS.Timeout>;
|
||||
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() {
|
||||
|
||||
@@ -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<void>;
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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,25 +38,31 @@ export default function VoiceChannel({
|
||||
return "h-14 w-14 text-lg";
|
||||
};
|
||||
|
||||
const handleJoinVoice = () => {
|
||||
appRef.current?.joinVoice(channel);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen gap-2 pb-3">
|
||||
<div className="relative flex flex-col h-screen gap-2 pb-3 overflow-hidden">
|
||||
{/* Header */}
|
||||
<header className="px-3 pt-3 pb-3 text-sm text-muted-foreground border-b border-b-border flex justify-between items-center">
|
||||
<header className="px-3 pt-3 pb-3 text-sm text-muted-foreground border-b border-b-border flex justify-between items-center z-10">
|
||||
<h2>{channel.name}</h2>
|
||||
<span className="text-xs">
|
||||
{usersLength} {usersLength === 1 ? "Participant" : "Participants"}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
{/* Main Grid */}
|
||||
{/* Main Grid Container */}
|
||||
<div className="relative flex-1 w-full min-h-0 overflow-hidden rounded-xl">
|
||||
{/* Active Grid - Fully readable at all times */}
|
||||
<div
|
||||
className={cn(
|
||||
"grid p-4 w-full gap-4 flex-1 auto-rows-fr bg-background/50 rounded-xl overflow-y-auto",
|
||||
"grid p-4 w-full h-full gap-4 auto-rows-fr bg-background/40 rounded-xl overflow-y-auto transition-opacity duration-300",
|
||||
getGridCols(),
|
||||
)}
|
||||
>
|
||||
{users?.map((pubkey) => {
|
||||
const user = appRef.current?.server?.users[pubkey];
|
||||
const user = server?.users[pubkey];
|
||||
const isSpeaking = speakers?.includes(pubkey);
|
||||
const displayName = user?.displayName || "Unknown User";
|
||||
const fallbackLetter = displayName.charAt(0).toUpperCase();
|
||||
@@ -72,7 +71,7 @@ export default function VoiceChannel({
|
||||
<div
|
||||
key={pubkey}
|
||||
className={cn(
|
||||
"relative flex flex-col items-center justify-center p-4 rounded-xl bg-card/80 transition-all duration-150 overflow-hidden group h-full",
|
||||
"relative flex flex-col items-center justify-center p-4 rounded-xl bg-card/90 transition-all duration-150 overflow-hidden group h-full",
|
||||
isSpeaking
|
||||
? "ring-2 ring-emerald-500 shadow-[0_0_15px_rgba(16,185,129,0.25)] bg-card"
|
||||
: "ring-1 ring-border/60 hover:ring-border",
|
||||
@@ -81,7 +80,10 @@ export default function VoiceChannel({
|
||||
{/* Avatar Container */}
|
||||
<div className="relative flex items-center justify-center">
|
||||
<Avatar
|
||||
className={cn("transition-all duration-200", getAvatarSize())}
|
||||
className={cn(
|
||||
"transition-all duration-200",
|
||||
getAvatarSize(),
|
||||
)}
|
||||
>
|
||||
<AvatarImage
|
||||
src={user?.avatar}
|
||||
@@ -102,6 +104,38 @@ export default function VoiceChannel({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Floating Join CTA Overlay */}
|
||||
{!isConnected && (
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-black/35 backdrop-brightness-75 pointer-events-auto transition-all animate-in fade-in duration-200">
|
||||
<div className="flex flex-col items-center gap-4 p-6 sm:p-8 rounded-2xl text-center max-w-xs sm:max-w-sm mx-4 transform scale-100 transition-all">
|
||||
<div className="p-3.5 rounded-full bg-emerald-500/10 text-emerald-500 border border-emerald-500/20">
|
||||
<Volume2 className="h-7 w-7 animate-pulse" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base sm:text-lg font-semibold text-foreground">
|
||||
Ready to join {channel.name}?
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{usersLength > 0
|
||||
? `${usersLength} ${usersLength === 1 ? "participant is" : "participants are"} currently connected.`
|
||||
: "Channel is currently empty."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleJoinVoice}
|
||||
size="lg"
|
||||
className="w-full bg-emerald-600 hover:bg-emerald-500 text-white font-medium shadow-lg shadow-emerald-600/20 transition-all"
|
||||
>
|
||||
<Phone />
|
||||
Join Voice
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -39,7 +39,7 @@ export default function StatusCard({
|
||||
return (
|
||||
<div className="absolute -left-16 right-16 bottom-0 w-[calc(100%+4rem-2rem)] z-10 ml-4 mb-4 @max-[150px]:hidden">
|
||||
<Card className="px-3 py-3 h-full w-full flex flex-col gap-0.5">
|
||||
{server?.isInVoiceChat && voiceChannel && (
|
||||
{voiceChannel && (
|
||||
<div className="text-emerald-500 flex flex-row items-center gap-2 border-b pb-2.5">
|
||||
<Signal className="h-5" />
|
||||
<div className="flex flex-col">
|
||||
|
||||
Reference in New Issue
Block a user