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