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/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() { )} - 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 abd9b9b..6f82d3d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,9 +1,9 @@ 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 { Toaster } from "@/components/ui/sonner"; +import { cookies } from "next/headers"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -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" }, @@ -34,9 +34,8 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const cookieStore = await cookies(); - - const settings = parseClientSettings(cookieStore.get("settings")?.value || 'black'); + const c = await cookies(); + const settings = parseClientSettings(c.get("settings")?.value); 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/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/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/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 (
diff --git a/src/components/channel/MessageBox.tsx b/src/components/channel/MessageBox.tsx index 110608c..2292031 100644 --- a/src/components/channel/MessageBox.tsx +++ b/src/components/channel/MessageBox.tsx @@ -392,9 +392,9 @@ export default function MessageBox({ )}
- {messages.toReversed().map((msg) => ( + {messages.toReversed().map((msg, i) => ( { if (!voiceConn) return; + if (!app.profile) return; - const stop = startMic(sendVoice); + const stop = startMic((data) => { + if (rms(data) > 0.005) { + 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 = 800; + + app.setSpeaking((prev) => + Object.fromEntries( + Object.entries(prev).filter(([_, ts]) => now - ts < TIMEOUT) + ) + ); + }, 200); + + 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/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/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..c82fe81 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 ) : ( - )} @@ -45,7 +40,7 @@ export default function SettingsDialog({
- + {profile && }
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..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); - }, 1000); + }, 800); // 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 d560ff9..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,8 +85,13 @@ export default async function auth({ switch (msg.type) { case "authenticated": - if (!onIndicator) break; - msg.params.indicators.forEach(onIndicator); + 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/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 { 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>; } 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 }> diff --git a/src/types/settings.ts b/src/types/settings.ts index 16e4936..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.voxa.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 ), };