Voice chat UI

This commit is contained in:
2026-08-27 17:53:40 +02:00
parent d7f9b32a99
commit ee71a99f5d
3 changed files with 84 additions and 11 deletions
+8 -1
View File
@@ -216,10 +216,17 @@ export default class Enclave<P = Page> {
server.voiceChatUsers[msg.channel_id] = [];
server.voiceChatUsers[msg.channel_id].push(msg.pubkey);
this.server?.getUsers([msg.pubkey]);
return;
case "Speaking":
server.voiceChatSpeaker(msg.pubkey);
clearTimeout(server.voiceChatSpeakers[msg.pubkey]);
server.voiceChatSpeakers[msg.pubkey] = setTimeout(() => {
delete server.voiceChatSpeakers[msg.pubkey];
this.forceRender();
}, 800);
this.forceRender();
return;
}
}
-8
View File
@@ -113,12 +113,4 @@ export default class EnclaveServer {
this.websocket?.send({ method: "GetUsers", pubkeys: filteredPubkeys });
}
public voiceChatSpeaker(pubkey: string) {
clearTimeout(this.voiceChatSpeakers[pubkey]);
this.voiceChatSpeakers[pubkey] = setTimeout(() => {
delete this.voiceChatSpeakers[pubkey];
}, 1000);
}
}
+76 -2
View File
@@ -2,6 +2,8 @@ import Enclave from "@/app/app";
import { ChannelPageProps } from "../PageView";
import { useEffect, useRef } from "react";
import { invoke } from "@tauri-apps/api/core";
import { cn } from "@/lib/utils";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
export default function VoiceChannel({
appRef,
@@ -35,13 +37,85 @@ export default function VoiceChannel({
});
}, [channel.id]);
const speakers =
appRef.current?.server?.voiceChatSpeakers &&
Object.keys(appRef.current?.server?.voiceChatSpeakers);
const users = appRef.current?.server?.voiceChatUsers[channel.id];
const usersLength = users?.length || 0;
// Dynamic grid column layout based on user count
const getGridCols = () => {
if (usersLength <= 3) return "grid-cols-1";
if (usersLength <= 6) return "grid-cols-3";
if (usersLength <= 9) return "grid-cols-3";
return "grid-cols-5";
};
// Avatar sizing scales up for fewer participants
const getAvatarSize = () => {
if (usersLength <= 2) return "h-24 w-24 text-3xl";
if (usersLength <= 4) return "h-20 w-20 text-2xl";
return "h-14 w-14 text-lg";
};
return (
<div className="flex flex-col h-screen gap-2">
<header className="px-3 pt-3 pb-3 text-sm text-muted-foreground border-b border-b-border">
<h2>{channel.name}</h2>
</header>
<div className="w-full h-full flex">
{JSON.stringify(appRef.current?.server?.voiceChatSpeakers)}
<div
className={cn(
"grid p-4 w-full gap-4 h-full auto-rows-fr bg-background/50 rounded-xl overflow-y-auto",
getGridCols(),
)}
>
{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();
return (
<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",
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",
)}
>
{/* Avatar Container */}
<div className="relative flex items-center justify-center">
<Avatar
className={cn("transition-all duration-200", getAvatarSize())}
>
<AvatarImage
src={user?.avatar}
alt={displayName}
className="object-cover"
/>
<AvatarFallback className="bg-muted font-bold text-muted-foreground">
{fallbackLetter}
</AvatarFallback>
</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 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}
</div>
</div>
);
})}
</div>
</div>
);