From 94238f5bdf98941c74428416d476147a0b4027da Mon Sep 17 00:00:00 2001 From: Leo dev Date: Wed, 7 Jan 2026 08:25:53 +0100 Subject: [PATCH 1/9] Rebranded to axiom --- README.md | 32 ++++++++++++++++---------------- package-lock.json | 4 ++-- package.json | 2 +- public/axiom-bg.svg | 4 ++++ public/axiom.svg | 3 +++ src/app/Shine.tsx | 20 +++++++++++++------- src/app/layout.tsx | 8 +++++--- src/app/page.tsx | 2 +- src/components/Logo.tsx | 8 +++----- src/types/settings.ts | 2 +- 10 files changed, 49 insertions(+), 36 deletions(-) create mode 100644 public/axiom-bg.svg create mode 100644 public/axiom.svg diff --git a/README.md b/README.md index d8dbe3c..cbedf73 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@


- VOXA + Axiom

Chat. Connect. Collaborate.

@@ -34,33 +34,33 @@ SUPABASE_SERVICE_ROLE_KEY="" ## Features -- Private: Messages can be handled by the server / node, voxa can't access them. +- Private: Messages can be handled by the server / node, axiom can't access them. - Customizable: Customize your server with plugins, a replacement for bots. ## Keywords -- Voxa Client: The user-side app which communicates with all below. -- Voxa Cloud: The authentication and user management handler. -- Voxa Server: The self-host-able community/friends communication service. -- Voxa Node: The self-host-able Private Message communication service, also used for sending notifications. +- Axiom Client: The user-side app which communicates with all below. +- Axiom Cloud: The authentication and user management handler. +- Axiom Server: The self-host-able community/friends communication service. +- Axiom Node: The self-host-able Private Message communication service, also used for sending notifications. ## Node Structure The Direct-Message structure works like Email, but real-time and the node uses a temporary auth, a node is what stores messages that are sent by a user. -This diagram shows that in order to send a message to a user, you must connect to their chosen node, which can be accessed via the Voxa Cloud and connected to directly. +This diagram shows that in order to send a message to a user, you must connect to their chosen node, which can be accessed via the Axiom Cloud and connected to directly. A node message can be a DM (Direct Message between two parties), or a SM (Server Message from a community). ```mermaid sequenceDiagram participant AliceClient as Alice's Client - participant Voxa as Voxa Cloud + participant Axiom as Axiom Cloud participant BobNode as Bob's Selected Node (Node2) participant BobClient as Bob's Client - AliceClient->>Voxa: Lookup Bob's node - Voxa-->>AliceClient: Respond with Bob → Node2 + AliceClient->>Axiom: Lookup Bob's node + Axiom-->>AliceClient: Respond with Bob → Node2 AliceClient->>BobNode: Authenticate and Send DM to bob BobNode-->>AliceClient: Acknowledge message stored BobNode->>BobClient: Notify new message @@ -78,7 +78,7 @@ flowchart TD A --> B{Is Bob connected
to this server?} B -- Yes --> C[Deliver directly
to Bob's client] C --> D[Ack received] - B -- No --> E[Lookup Bob's selected node
via Voxa Cloud] + B -- No --> E[Lookup Bob's selected node
via Axiom Cloud] E --> F[Forward message
to Bob's node] F --> G[Bob's node notifies
Bob's client] G --> H[Ack flows back] @@ -91,14 +91,14 @@ This sequence happens when you connect to a server or node. ```mermaid sequenceDiagram participant Alice as Alice's Client - participant Voxa as Voxa Cloud + participant Axiom as Axiom Cloud participant Server as Node / Server - Alice->>Voxa: Register token for the (Node / Server) - Voxa-->>Alice: TOKEN + Alice->>Axiom: Register token for the (Node / Server) + Axiom-->>Alice: TOKEN Alice->>Server: Authenticate with TOKEN - Server->>Voxa: Authenticate with TOKEN - Voxa-->>Server: The user is Alice + Server->>Axiom: Authenticate with TOKEN + Axiom-->>Server: The user is Alice Server-->>Alice: You are Alice ``` diff --git a/package-lock.json b/package-lock.json index 1bea6d6..2d8a247 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "voxa", + "name": "axiom", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "voxa", + "name": "axiom", "version": "0.1.0", "dependencies": { "@lexical/react": "^0.34.0", diff --git a/package.json b/package.json index d89a45b..c2c9bd4 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "voxa", + "name": "axiom", "version": "0.1.0", "private": true, "scripts": { diff --git a/public/axiom-bg.svg b/public/axiom-bg.svg new file mode 100644 index 0000000..31ebbad --- /dev/null +++ b/public/axiom-bg.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/axiom.svg b/public/axiom.svg new file mode 100644 index 0000000..44a2360 --- /dev/null +++ b/public/axiom.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/Shine.tsx b/src/app/Shine.tsx index bf086b6..9d44842 100644 --- a/src/app/Shine.tsx +++ b/src/app/Shine.tsx @@ -1,59 +1,65 @@ export default function Shine() { return (
+ {/* Primary Deep Glow */}
+ {/* Secondary Soft Glow */}
+ {/* Inner Purple Tint */}
+ {/* Core Highlight Line (Brightest) */}
+ {/* Sharp Center Line */}
+ {/* Large Ambient Background Blur */}
); -} +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index abd9b9b..c218aa5 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -16,9 +16,9 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Voxa | Your Conversations, Your Privacy, Your Community.", + title: "Axiom | Your Conversations, Your Privacy, Your Community.", description: - "Voxa is a modern chat app built for speed, simplicity, and collaboration. Experience real-time messaging with powerful tools for teams and communities.", + "Axiom is a modern chat app built for speed, simplicity, and collaboration. Experience real-time messaging with powerful tools for teams and communities.", icons: { icon: [ { url: "/favicon.ico" }, @@ -36,7 +36,9 @@ export default async function RootLayout({ }>) { const cookieStore = await cookies(); - const settings = parseClientSettings(cookieStore.get("settings")?.value || 'black'); + const settings = parseClientSettings( + cookieStore.get("settings")?.value || "black" + ); return ( diff --git a/src/app/page.tsx b/src/app/page.tsx index 48c2359..60c706b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -96,7 +96,7 @@ export default function LandingPage() { {/* Footer */} ); diff --git a/src/components/Logo.tsx b/src/components/Logo.tsx index 60e531b..e587445 100644 --- a/src/components/Logo.tsx +++ b/src/components/Logo.tsx @@ -7,16 +7,14 @@ export default function Logo(props: Props) { ); diff --git a/src/types/settings.ts b/src/types/settings.ts index 16e4936..1cac9b1 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -23,7 +23,7 @@ export const PROFILE_SCHEMA: SettingsSchema = { display_name: "string", avatar_url: "string", node_address: new RegexSetting( - "node2.voxa.org", + "node2.axiom.org", /^(?:(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(?:\d{1,3}\.){3}\d{1,3})$/ ), }; From 943be3e2fb2ad1b9f99b4e8edda7a33652275ca8 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Wed, 7 Jan 2026 09:40:36 +0100 Subject: [PATCH 2/9] Fixed colors --- src/app/globals.css | 10 +++++----- src/app/layout.tsx | 9 ++------- src/lib/clientSettings.ts | 2 +- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index c4d43b4..89685b1 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -51,7 +51,7 @@ --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); + --primary: oklch(56.008% 0.28326 298.234); --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.97 0 0); --secondary-foreground: oklch(0.205 0 0); @@ -85,8 +85,8 @@ --card-foreground: oklch(0.985 0 0); --popover: oklch(0.205 0 0); --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); + --primary: oklch(63.261% 0.23681 303.14); + --primary-foreground: oklch(0.922 0 0); --secondary: oklch(0.269 0 0); --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.269 0 0); @@ -119,8 +119,8 @@ --card-foreground: oklch(0.985 0 0); --popover: oklch(0.150 0 0); --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.150 0 0); + --primary: oklch(63.261% 0.23681 303.14); + --primary-foreground: oklch(0.922 0 0); --secondary: oklch(0.269 0 0); --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.215 0 0); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c218aa5..7bbefb3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,8 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; -import { cookies } from "next/headers"; -import { parseClientSettings } from "@/lib/clientSettings"; +import { getClientSettings } from "@/lib/clientSettings"; import { Toaster } from "@/components/ui/sonner"; const geistSans = Geist({ @@ -34,11 +33,7 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const cookieStore = await cookies(); - - const settings = parseClientSettings( - cookieStore.get("settings")?.value || "black" - ); + const settings = getClientSettings(); return ( diff --git a/src/lib/clientSettings.ts b/src/lib/clientSettings.ts index 4ba3a1b..484dcdf 100644 --- a/src/lib/clientSettings.ts +++ b/src/lib/clientSettings.ts @@ -2,7 +2,7 @@ import { ClientSettings } from "@/types/settings"; import Cookies from "js-cookie"; export const defaultSettings: ClientSettings = { - theme: "black", + theme: "dark", }; export function parseClientSettings(s?: string): ClientSettings { From dd65dd0c242a281d560ac5a42f1645fdbe46bf66 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Wed, 7 Jan 2026 10:26:49 +0100 Subject: [PATCH 3/9] Password peek --- src/app/auth/Login.tsx | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/app/auth/Login.tsx b/src/app/auth/Login.tsx index 6c1e61d..31e8a2e 100644 --- a/src/app/auth/Login.tsx +++ b/src/app/auth/Login.tsx @@ -7,6 +7,7 @@ import { useRouter } from "next/navigation"; import { CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { loginUser } from "@/actions/user"; import Cookies from "js-cookie"; +import { EyeIcon, EyeOffIcon } from "lucide-react"; type Feedback = { kind: "error" | "info"; message: string }; @@ -14,6 +15,7 @@ export default function Login() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [feedback, setFeedback] = useState(); + const [passwordVisible, setPasswordVisible] = useState(false); const router = useRouter(); @@ -54,14 +56,29 @@ export default function Login() { Password - setPassword(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") login(); - }} - type="password" - /> +
+ setPassword(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") login(); + }} + type={passwordVisible ? "text" : "password"} + /> +
+
setPasswordVisible((prev) => !prev)} + > + {passwordVisible ? ( + + ) : ( + + )} +
+
+
{feedback && ( @@ -76,7 +93,7 @@ export default function Login() { )} - From 3476b2971359bcce8270ac0b6a4d61d29c1500b5 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Wed, 7 Jan 2026 16:26:19 +0100 Subject: [PATCH 4/9] Fixed theme and settings --- src/app/layout.tsx | 6 ++++-- src/components/settings/SettingSchema.tsx | 4 ++-- src/components/settings/SettingsDialog.tsx | 2 +- src/types/settings.ts | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7bbefb3..6f82d3d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,8 +1,9 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; -import { getClientSettings } from "@/lib/clientSettings"; +import { parseClientSettings } from "@/lib/clientSettings"; import { Toaster } from "@/components/ui/sonner"; +import { cookies } from "next/headers"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -33,7 +34,8 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const settings = getClientSettings(); + const c = await cookies(); + const settings = parseClientSettings(c.get("settings")?.value); return ( diff --git a/src/components/settings/SettingSchema.tsx b/src/components/settings/SettingSchema.tsx index 840c1dc..d28be42 100644 --- a/src/components/settings/SettingSchema.tsx +++ b/src/components/settings/SettingSchema.tsx @@ -58,7 +58,7 @@ export default function SettingSchema({ if (!regexSetting.pattern.test(value)) { setFeedback({ kind: "error", - message: `Value does not match the pattern!`, + message: `Value does not match the pattern`, }); } else { setFeedback(undefined); @@ -124,7 +124,7 @@ export default function SettingSchema({ <> {Object.entries(schema).map(([key, type]) => (
- + {renderInput(key, type)}
))} diff --git a/src/components/settings/SettingsDialog.tsx b/src/components/settings/SettingsDialog.tsx index 12c6789..1e8d452 100644 --- a/src/components/settings/SettingsDialog.tsx +++ b/src/components/settings/SettingsDialog.tsx @@ -45,7 +45,7 @@ export default function SettingsDialog({
- + {profile && }
diff --git a/src/types/settings.ts b/src/types/settings.ts index 1cac9b1..75d02a2 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -23,7 +23,7 @@ export const PROFILE_SCHEMA: SettingsSchema = { display_name: "string", avatar_url: "string", node_address: new RegexSetting( - "node2.axiom.org", - /^(?:(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(?:\d{1,3}\.){3}\d{1,3})$/ + "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i ), }; From 0970617385ddcecb44572172f32e5071322584d0 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Wed, 7 Jan 2026 16:31:56 +0100 Subject: [PATCH 5/9] Small coloring fix --- src/components/app/SidebarServers.tsx | 7 +------ src/components/settings/SettingsDialog.tsx | 9 ++------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/components/app/SidebarServers.tsx b/src/components/app/SidebarServers.tsx index c6a4502..518cd9c 100644 --- a/src/components/app/SidebarServers.tsx +++ b/src/components/app/SidebarServers.tsx @@ -17,8 +17,6 @@ import { Input } from "../ui/input"; import { Label } from "../ui/label"; import { toast } from "sonner"; import { useState } from "react"; -import { get } from "@/lib/request"; -import { Server } from "@/types/types"; import App from "@/types/app"; import ProfilePicture from "../ProfilePicture"; import SettingsDialog from "../settings/SettingsDialog"; @@ -43,10 +41,7 @@ export default function SidebarServers({ app }: { app: App }) { return (
- diff --git a/src/components/settings/SettingsDialog.tsx b/src/components/settings/SettingsDialog.tsx index 1e8d452..3a25018 100644 --- a/src/components/settings/SettingsDialog.tsx +++ b/src/components/settings/SettingsDialog.tsx @@ -10,7 +10,7 @@ import { import { cn } from "@/lib/utils"; import Settings from "./Settings"; import { ProfileSettings } from "@/types/settings"; -import { ReactNode, useState } from "react"; +import { ReactNode } from "react"; export default function SettingsDialog({ className, @@ -29,12 +29,7 @@ export default function SettingsDialog({ {children ? ( children ) : ( - )} From 2c266742b2a9620a9069c10870eed6c367a3f079 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Wed, 7 Jan 2026 16:36:58 +0100 Subject: [PATCH 6/9] Multi theme coloring fix --- src/components/app/SidebarServers.tsx | 5 ++++- src/components/settings/SettingsDialog.tsx | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/app/SidebarServers.tsx b/src/components/app/SidebarServers.tsx index 518cd9c..e1bdce1 100644 --- a/src/components/app/SidebarServers.tsx +++ b/src/components/app/SidebarServers.tsx @@ -41,7 +41,10 @@ export default function SidebarServers({ app }: { app: App }) { return (
- diff --git a/src/components/settings/SettingsDialog.tsx b/src/components/settings/SettingsDialog.tsx index 3a25018..c82fe81 100644 --- a/src/components/settings/SettingsDialog.tsx +++ b/src/components/settings/SettingsDialog.tsx @@ -29,7 +29,7 @@ export default function SettingsDialog({ {children ? ( children ) : ( - )} From 1577ce38e10d59e62f0c94365e944fc7faca0c34 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Wed, 7 Jan 2026 17:10:36 +0100 Subject: [PATCH 7/9] Initial voice chat #42 --- src/lib/auth.ts | 6 ++++++ src/types/protocol.ts | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index d560ff9..032a3e4 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -84,6 +84,12 @@ export default async function auth({ case "authenticated": if (!onIndicator) break; msg.params.indicators.forEach(onIndicator); + if (!onVoiceJoin) break; + Object.entries(msg.params.voice_chat).forEach(([channelId, users]) => + Object.entries(users).forEach(([userId, voiceId]) => + onVoiceJoin(userId, channelId, voiceId) + ) + ); break; case "message_create": diff --git a/src/types/protocol.ts b/src/types/protocol.ts index 577d99c..c06529b 100644 --- a/src/types/protocol.ts +++ b/src/types/protocol.ts @@ -25,7 +25,14 @@ export type ClientMessage = | EnumType<"leave_voice", { channel_id: string }>; export type ServerMessage = - | EnumType<"authenticated", { uuid: string; indicators: IndicatorContext[] }> + | EnumType< + "authenticated", + { + uuid: string; + indicators: IndicatorContext[]; + voice_chat: Record>; + } + > | EnumType<"temp_message", { message: string }> | EnumType<"message_create", Message> | EnumType<"message_update", { message_id: number; contents: string }> From f5769716e98e8df66ff5a7253be5423b233aac56 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Wed, 7 Jan 2026 18:41:34 +0100 Subject: [PATCH 8/9] Speaking indicator --- src/app/server/[ip]/page.tsx | 5 +++ src/components/channel/voice/VoiceBox.tsx | 40 ++++++++++++++++++++--- src/hooks/use-app.ts | 8 +++-- src/lib/audio.ts | 11 ++++++- src/lib/auth.ts | 18 +++++----- src/types/app.ts | 3 ++ 6 files changed, 69 insertions(+), 16 deletions(-) diff --git a/src/app/server/[ip]/page.tsx b/src/app/server/[ip]/page.tsx index 1d81717..9e3b012 100644 --- a/src/app/server/[ip]/page.tsx +++ b/src/app/server/[ip]/page.tsx @@ -41,6 +41,11 @@ export default function Server() { [channelId]: remainingUsers, }; }), + onSpeaking: (voiceId) => + app.setSpeaking((speaking) => ({ + ...speaking, + [voiceId]: Date.now(), + })), }); }, [ip, messageStore.messages], diff --git a/src/components/channel/voice/VoiceBox.tsx b/src/components/channel/voice/VoiceBox.tsx index 388f661..e2c1ccb 100644 --- a/src/components/channel/voice/VoiceBox.tsx +++ b/src/components/channel/voice/VoiceBox.tsx @@ -4,7 +4,7 @@ import App from "@/types/app"; import { ClientMessage, IndicatorContext } from "@/types/protocol"; import { Message } from "@/types/types"; import { useEffect, useState } from "react"; -import { startMic } from "@/lib/audio"; +import { rms, startMic } from "@/lib/audio"; import { Button } from "../../ui/button"; import { ChevronLeftIcon, Phone, Volume2Icon } from "lucide-react"; import VoiceGrid from "./VoiceGrid"; @@ -28,13 +28,43 @@ export default function VoiceBox({ useEffect(() => { if (!voiceConn) return; + if (!app.profile) return; - const stop = startMic(sendVoice); + const stop = startMic((data) => { + if (rms(data) > 0.02) { + sendVoice(data); + + if (!app.profile) return; + + const voiceId = app.voiceConns[channelId]?.[app.profile.id]; + if (!voiceId) return; + + app.setSpeaking((prev) => ({ + ...prev, + [voiceId]: Date.now(), + })); + } + }); return () => { stop.then((s) => s()); }; - }, [voiceConn]); + }, [voiceConn, channelId, app.profile?.id, app.voiceConns]); + + useEffect(() => { + const inv = setInterval(() => { + const now = Date.now(); + const TIMEOUT = 500; + + app.setSpeaking((prev) => + Object.fromEntries( + Object.entries(prev).filter(([_, ts]) => now - ts < TIMEOUT) + ) + ); + }, 250); + + return () => clearInterval(inv); + }, []); return (
@@ -53,9 +83,9 @@ export default function VoiceBox({ {app.voiceConns[channelId] && ( ({ + ([userId, voiceId]) => ({ id: userId, - speaking: false, + speaking: Boolean(app.speaking[voiceId]), }) )} app={app} diff --git a/src/hooks/use-app.ts b/src/hooks/use-app.ts index 7f6f464..9e71dca 100644 --- a/src/hooks/use-app.ts +++ b/src/hooks/use-app.ts @@ -21,10 +21,12 @@ export default function useApp(): App { const [currentChannel, setCurrentChannel] = useState(null); const [server, setServer] = useState(); const privateMessages = useMessages(); - const [voiceConns, SetVoiceConns] = useState< + const [voiceConns, setVoiceConns] = useState< Record> >({}); + const [speaking, setSpeaking] = useState>({}); + useEffect(() => { const stored = Cookies.get("servers"); if (!stored) return; @@ -40,8 +42,10 @@ export default function useApp(): App { }, []); return { + speaking, + setSpeaking, voiceConns, - setVoiceConns: SetVoiceConns, + setVoiceConns, privateMessages, currentChannel, setCurrentChannel, diff --git a/src/lib/audio.ts b/src/lib/audio.ts index 1f62d94..8c1e620 100644 --- a/src/lib/audio.ts +++ b/src/lib/audio.ts @@ -42,7 +42,7 @@ export async function startMic(onAudio: (data: Int16Array) => void) { chunk.set(buffer.splice(0, buffer.length)); } onAudio(chunk); - }, 1000); + }, 500); // Return cleanup function return () => { @@ -101,3 +101,12 @@ export function playPCM16(u8: Uint8Array) { src.connect(audioCtx.destination); src.start(); } + +export function rms(samples: Int16Array): number { + let sum = 0; + for (let i = 0; i < samples.length; i++) { + const s = samples[i] / 32768; // normalize to [-1, 1] + sum += s * s; + } + return Math.sqrt(sum / samples.length); +} diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 032a3e4..9368cae 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -17,6 +17,7 @@ type AuthParams = { onVoice?: (userId: string, bytes: number[]) => void; onVoiceJoin?: (userId: string, channelId: string, voiceId: number) => void; onVoiceLeave?: (userId: string, channelId: string, voiceId: number) => void; + onSpeaking?: (voiceId: number) => void; }; export default async function auth({ @@ -28,6 +29,7 @@ export default async function auth({ onIndicator, onVoiceJoin, onVoiceLeave, + onSpeaking, }: AuthParams) { console.log("Authenticating with server id:", id); @@ -53,6 +55,7 @@ export default async function auth({ const buffer = await m.data.arrayBuffer(); const bytes = new Uint8Array(buffer); playPCM16(bytes); + if (onSpeaking) onSpeaking(new DataView(buffer).getUint16(0, true)); return; } @@ -82,14 +85,13 @@ export default async function auth({ switch (msg.type) { case "authenticated": - if (!onIndicator) break; - msg.params.indicators.forEach(onIndicator); - if (!onVoiceJoin) break; - Object.entries(msg.params.voice_chat).forEach(([channelId, users]) => - Object.entries(users).forEach(([userId, voiceId]) => - onVoiceJoin(userId, channelId, voiceId) - ) - ); + if (onIndicator) msg.params.indicators.forEach(onIndicator); + if (onVoiceJoin) + Object.entries(msg.params.voice_chat).forEach(([channelId, users]) => + Object.entries(users).forEach(([userId, voiceId]) => + onVoiceJoin(userId, channelId, voiceId) + ) + ); break; case "message_create": diff --git a/src/types/app.ts b/src/types/app.ts index 6fac9b5..b9304f2 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -38,4 +38,7 @@ export default interface App { voiceConns: Record>; setVoiceConns: SetState>>; + + speaking: Record; + setSpeaking: SetState>; } From 66d390b03dbc60d8e4992ebfda76022057b15585 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Wed, 7 Jan 2026 19:36:05 +0100 Subject: [PATCH 9/9] Improved vc ui and possibly fixed #41 --- src/components/app/AppLayout.tsx | 2 ++ src/components/channel/MessageBox.tsx | 4 ++-- src/components/channel/voice/VoiceBox.tsx | 6 +++--- src/components/channel/voice/VoiceGrid.tsx | 14 +++++++------- src/components/channel/voice/VoiceTile.tsx | 11 ++--------- src/lib/audio.ts | 2 +- 6 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/components/app/AppLayout.tsx b/src/components/app/AppLayout.tsx index 164250e..b3a696c 100644 --- a/src/components/app/AppLayout.tsx +++ b/src/components/app/AppLayout.tsx @@ -66,6 +66,8 @@ export default function AppLayout({ [undefined, undefined] ); + useEffect(() => app.setSidebarOpen(!isMobile), [isMobile]); + return (
- {messages.toReversed().map((msg) => ( + {messages.toReversed().map((msg, i) => ( { - if (rms(data) > 0.02) { + if (rms(data) > 0.005) { sendVoice(data); if (!app.profile) return; @@ -54,14 +54,14 @@ export default function VoiceBox({ useEffect(() => { const inv = setInterval(() => { const now = Date.now(); - const TIMEOUT = 500; + const TIMEOUT = 800; app.setSpeaking((prev) => Object.fromEntries( Object.entries(prev).filter(([_, ts]) => now - ts < TIMEOUT) ) ); - }, 250); + }, 200); return () => clearInterval(inv); }, []); diff --git a/src/components/channel/voice/VoiceGrid.tsx b/src/components/channel/voice/VoiceGrid.tsx index 340e1ff..904b5b9 100644 --- a/src/components/channel/voice/VoiceGrid.tsx +++ b/src/components/channel/voice/VoiceGrid.tsx @@ -1,5 +1,6 @@ import App from "@/types/app"; import { VoiceTile, VoiceUser } from "./VoiceTile"; +import { cn } from "@/lib/utils"; export default function VoiceGrid({ app, @@ -11,13 +12,12 @@ export default function VoiceGrid({ return (
{users.map((user) => ( diff --git a/src/components/channel/voice/VoiceTile.tsx b/src/components/channel/voice/VoiceTile.tsx index a914859..f1e4246 100644 --- a/src/components/channel/voice/VoiceTile.tsx +++ b/src/components/channel/voice/VoiceTile.tsx @@ -12,13 +12,11 @@ export function VoiceTile({ user, app }: { app: App; user: VoiceUser }) {
)} /> - - {/* Speaking indicator */} - {user.speaking && ( -
Speaking
- )}
); } diff --git a/src/lib/audio.ts b/src/lib/audio.ts index 8c1e620..4f96665 100644 --- a/src/lib/audio.ts +++ b/src/lib/audio.ts @@ -42,7 +42,7 @@ export async function startMic(onAudio: (data: Int16Array) => void) { chunk.set(buffer.splice(0, buffer.length)); } onAudio(chunk); - }, 500); + }, 800); // Return cleanup function return () => {