Working UI

This commit is contained in:
2026-01-06 20:27:37 +01:00
parent e40884ba7f
commit df16effa4b
8 changed files with 89 additions and 41 deletions
+13
View File
@@ -28,6 +28,19 @@ export default function Server() {
setServer: app.setServer,
messageStore: messageStore,
onIndicator: addIndicator,
onVoiceJoin: (userId, channelId, voiceId) =>
app.setVoiceConns((prev) => ({
...prev,
[channelId]: { ...prev[channelId], [userId]: voiceId },
})),
onVoiceLeave: (userId, channelId) =>
app.setVoiceConns((prev) => {
const { [userId]: _, ...remainingUsers } = prev[channelId] || {};
return {
...prev,
[channelId]: remainingUsers,
};
}),
});
},
[ip, messageStore.messages],
+1
View File
@@ -47,6 +47,7 @@ export default function ChannelView({
<VoiceBox
app={app}
channelName={app.currentChannel.name}
channelId={app.currentChannel.id}
messages={messages}
sendRequest={(req) => {
while (serverRef.current?.CONNECTING) {}
+17 -22
View File
@@ -6,13 +6,7 @@ import { Message } from "@/types/types";
import { useEffect, useState } from "react";
import { startMic } from "@/lib/audio";
import { Button } from "../../ui/button";
import {
ChevronLeftIcon,
Phone,
PhoneCall,
PhoneOff,
Volume2Icon,
} from "lucide-react";
import { ChevronLeftIcon, Phone, Volume2Icon } from "lucide-react";
import VoiceGrid from "./VoiceGrid";
export default function VoiceBox({
@@ -20,10 +14,12 @@ export default function VoiceBox({
app,
channelName,
sendRequest,
channelId,
}: {
channelName?: string;
app: App;
sendRequest: (req: ClientMessage) => void;
channelName?: string;
channelId: string;
messages: Message[];
indicators: IndicatorContext[];
sendVoice: (data: Int16Array) => void;
@@ -54,25 +50,26 @@ export default function VoiceBox({
</span>
</header>
)}
<VoiceGrid
users={[
{ id: "1", name: "Klesty", speaking: true },
{ id: "2", name: "Alex", speaking: false },
{ id: "3", name: "Mia", speaking: true },
{ id: "4", name: "John", speaking: false },
]}
/>
{app.voiceConns[channelId] && (
<VoiceGrid
users={Object.entries(app.voiceConns[channelId]).map(
([userId, _]) => ({
id: userId,
speaking: false,
})
)}
app={app}
/>
)}
<footer className="mt-auto h-max w-max mx-auto">
{voiceConn ? (
<Button
className="mx-auto my-auto h-max bg-transparent text-destructive border border-destructive hover:bg-destructive/20 rounded-full aspect-square w-12"
onClick={() => {
if (!app.currentChannel) return;
setVoiceConn(false);
sendRequest({
type: "leave_voice",
params: { channel_id: app.currentChannel.id },
params: { channel_id: channelId },
});
}}
>
@@ -82,12 +79,10 @@ export default function VoiceBox({
<Button
className="mx-auto my-auto h-max bg-transparent text-chart-2 border border-chart-2 hover:bg-chart-2/20 rounded-full aspect-square w-12"
onClick={() => {
if (!app.currentChannel) return;
setVoiceConn(true);
sendRequest({
type: "join_voice",
params: { channel_id: app.currentChannel.id },
params: { channel_id: channelId },
});
}}
>
+9 -2
View File
@@ -1,6 +1,13 @@
import App from "@/types/app";
import { VoiceTile, VoiceUser } from "./VoiceTile";
export default function VoiceGrid({ users }: { users: VoiceUser[] }) {
export default function VoiceGrid({
app,
users,
}: {
app: App;
users: VoiceUser[];
}) {
return (
<div className="h-full overflow-y-auto p-4">
<div
@@ -13,7 +20,7 @@ export default function VoiceGrid({ users }: { users: VoiceUser[] }) {
"
>
{users.map((user) => (
<VoiceTile key={user.id} user={user} />
<VoiceTile key={user.id} user={user} app={app} />
))}
</div>
</div>
+20 -15
View File
@@ -1,11 +1,13 @@
import AsyncValue from "@/components/AsyncValue";
import App from "@/types/app";
export type VoiceUser = {
id: string;
name: string;
speaking: boolean;
muted?: boolean;
};
export function VoiceTile({ user }: { user: VoiceUser }) {
export function VoiceTile({ user, app }: { app: App; user: VoiceUser }) {
return (
<div
className={`
@@ -21,20 +23,23 @@ export function VoiceTile({ user }: { user: VoiceUser }) {
${user.muted ? "opacity-50" : ""}
`}
>
{/* Avatar */}
<div
className={`
h-20 w-20 rounded-full
flex items-center justify-center
text-2xl font-semibold
bg-background
`}
>
{user.name[0].toUpperCase()}
</div>
{/* Avatar and name */}
<AsyncValue
func={() => app.getUserById(user.id)}
render={({ value }) => (
<>
<img
src={value?.avatar_url}
alt="Pfp"
className="h-20 w-20 rounded-full"
/>
{/* Name */}
<div className="mt-3 text-sm font-medium">{user.name}</div>
<div className="mt-3 text-sm font-medium">
{value?.display_name}
</div>
</>
)}
/>
{/* Speaking indicator */}
{user.speaking && (
+5 -1
View File
@@ -4,7 +4,6 @@ import useMessages from "./use-messages";
import { StringMap } from "@/types/typeUtils";
import useUser, { UserProfile } from "./get-user";
import { useClientSettings } from "./use-settings";
import { get, Response } from "@/lib/request";
import { Channel, Server } from "@/types/types";
import Cookies from "js-cookie";
import { toast } from "sonner";
@@ -22,6 +21,9 @@ export default function useApp(): App {
const [currentChannel, setCurrentChannel] = useState<Channel | null>(null);
const [server, setServer] = useState<Server>();
const privateMessages = useMessages();
const [voiceConns, SetVoiceConns] = useState<
Record<string, Record<string, number>>
>({});
useEffect(() => {
const stored = Cookies.get("servers");
@@ -38,6 +40,8 @@ export default function useApp(): App {
}, []);
return {
voiceConns,
setVoiceConns: SetVoiceConns,
privateMessages,
currentChannel,
setCurrentChannel,
+21 -1
View File
@@ -15,6 +15,8 @@ type AuthParams = {
messageStore: MessageStore;
onIndicator?: (i: IndicatorContext) => void;
onVoice?: (userId: string, bytes: number[]) => void;
onVoiceJoin?: (userId: string, channelId: string, voiceId: number) => void;
onVoiceLeave?: (userId: string, channelId: string, voiceId: number) => void;
};
export default async function auth({
@@ -24,6 +26,8 @@ export default async function auth({
onNewMessage,
messageStore,
onIndicator,
onVoiceJoin,
onVoiceLeave,
}: AuthParams) {
console.log("Authenticating with server id:", id);
@@ -48,7 +52,7 @@ export default async function auth({
if (m.data instanceof Blob) {
const buffer = await m.data.arrayBuffer();
const bytes = new Uint8Array(buffer);
playPCM16(bytes)
playPCM16(bytes);
return;
}
@@ -101,6 +105,22 @@ export default async function auth({
if (!onIndicator) break;
onIndicator(msg.params);
break;
case "voice_join":
if (!onVoiceJoin) break;
onVoiceJoin(
msg.params.user_id,
msg.params.channel_id,
msg.params.voice_id
);
break;
case "voice_leave":
if (!onVoiceLeave) break;
onVoiceLeave(
msg.params.user_id,
msg.params.channel_id,
msg.params.voice_id
);
break;
}
};
+3
View File
@@ -35,4 +35,7 @@ export default interface App {
currentChannel: Channel | null;
setCurrentChannel: SetState<Channel | null>;
voiceConns: Record<string, Record<string, number>>;
setVoiceConns: SetState<Record<string, Record<string, number>>>;
}