From 550a4cc04ba9c0999ea88582ed3d29bb05b87ea0 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 9 Jan 2026 18:28:07 +0100 Subject: [PATCH 1/8] Fixed 404 issue #39 --- src/app/chat/StartNewDM.tsx | 12 ++++++++---- src/components/app/SidebarDMs.tsx | 13 +++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/app/chat/StartNewDM.tsx b/src/app/chat/StartNewDM.tsx index bfdca60..4002896 100644 --- a/src/app/chat/StartNewDM.tsx +++ b/src/app/chat/StartNewDM.tsx @@ -13,16 +13,20 @@ import { PlusCircle } from "lucide-react"; import { UserProfile } from "@/hooks/get-user"; import { useRouter } from "next/navigation"; import axios from "axios"; +import { getProfileByUsername } from "@/actions/get-profile"; +import { toast } from "sonner"; export default function StartNewDM() { const [username, setUsername] = useState(""); const router = useRouter(); const handleStartDM = async () => { - const res = await axios.get("/api/profile", { - params: { username }, - }); - const user = res.data as UserProfile; + const user = await getProfileByUsername(username); + if (!user) { + toast.error("User not found"); + return; + } + router.push(`/chat/${user.id}`); setUsername(""); }; diff --git a/src/components/app/SidebarDMs.tsx b/src/components/app/SidebarDMs.tsx index a059b9c..b82eedc 100644 --- a/src/components/app/SidebarDMs.tsx +++ b/src/components/app/SidebarDMs.tsx @@ -10,6 +10,8 @@ import DMItem from "./DMItem"; import App from "@/types/app"; import { UserProfile } from "@/hooks/get-user"; import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { getProfileByUsername } from "@/actions/get-profile"; export default function SidebarDMs({ app }: { app: App }) { const router = useRouter(); @@ -78,11 +80,14 @@ export default function SidebarDMs({ app }: { app: App }) { variant="ghost" className="w-full justify-start gap-2 text-sm truncate" onClick={async () => { - const res = await axios.get("/api/profile", { - params: { username: query }, - }); - const user = res.data as UserProfile; + const user = await getProfileByUsername(query); + if (!user) { + toast.error("User not found"); + return; + } + router.push(`/chat/${user.id}`); + setQuery(""); }} > From e2bddec5256b6f933fa17802123a6cc3c96450b6 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 9 Jan 2026 18:46:24 +0100 Subject: [PATCH 2/8] Trying to fix DMs --- src/app/chat/[id]/page.tsx | 56 ++++++++++++++++++++------------------ src/app/chat/page.tsx | 1 - src/lib/auth.ts | 21 ++++++++++++++ 3 files changed, 50 insertions(+), 28 deletions(-) diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index 7053bb2..1920354 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -5,11 +5,12 @@ import AppLayout from "@/components/app/AppLayout"; import { useParams, useRouter } from "next/navigation"; import useApp from "@/hooks/use-app"; import { useEffect, useRef, useState } from "react"; -import auth from "@/lib/auth"; +import auth, { waitForOpen } from "@/lib/auth"; import useAsync from "@/hooks/use-async"; import { Button } from "@/components/ui/button"; import useIndicators from "@/hooks/use-indicators"; import { useEffectOnceWhenReady } from "@/hooks/use-once"; +import { ClientMessage } from "@/types/protocol"; export default function DMs() { const { id } = useParams<{ id: string }>(); @@ -19,11 +20,30 @@ export default function DMs() { const router = useRouter(); const { indicators, addIndicator } = useIndicators(); + const sendRequest = async (req: ClientMessage) => { + console.log("Sending request: ", req); + await waitForOpen(targetNode.current); + await waitForOpen(app.node.current); + + switch (req.type) { + case "send_message": + targetNode.current?.send(JSON.stringify(req)); + break; + case "load_chunk": + targetNode.current?.send(JSON.stringify(req)); + app.node.current?.send(JSON.stringify(req)); + break; + default: + app.node.current?.send(JSON.stringify(req)); + break; + } + }; + useEffectOnceWhenReady( () => { async function connectTargetNode() { if (!target) return; - auth({ + await auth({ id: target.node_address, wsRef: targetNode, messageStore: app.privateMessages, @@ -31,12 +51,12 @@ export default function DMs() { }); } - connectTargetNode(); - - app.setCurrentChannel({ - id: id, - name: target?.display_name || id, - kind: "text", + connectTargetNode().then(() => { + app.setCurrentChannel({ + id: id, + name: target?.display_name || id, + kind: "text", + }); }); }, [target], @@ -54,25 +74,7 @@ export default function DMs() { (m.channel_id === id && m.from === app.profile?.id) || (m.from === id && m.channel_id === app.profile?.id) )} - sendRequest={(req) => { - console.log("Sending request: ", req); - while ( - targetNode.current?.CONNECTING || - app.node.current?.CONNECTING - ) {} - switch (req.type) { - case "send_message": - targetNode.current?.send(JSON.stringify(req)); - break; - case "load_chunk": - targetNode.current?.send(JSON.stringify(req)); - app.node.current?.send(JSON.stringify(req)); - break; - default: - app.node.current?.send(JSON.stringify(req)); - break; - } - }} + sendRequest={sendRequest} indicators={indicators.filter( (indicator) => indicator.indicator.params.user_id !== app.profile?.id )} diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index 050a516..8918939 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -33,7 +33,6 @@ export default function ChatHub() { const app = useApp(); const recentPeople = app.dms.slice(0, 3); - const activeServers: Server[] = Object.values(app.servers).slice(0, 3); const trendingServers: Server[] = [ { id: "7de3138f-e0b6-4d96-8158-8c7e866ba0ff", diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 9368cae..155c4b5 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -163,3 +163,24 @@ export default async function auth({ ws.close(); }; } + +export async function waitForOpen(ws: WebSocket | null): Promise { + if (ws?.readyState === WebSocket.OPEN) return Promise.resolve(); + + return new Promise((resolve, reject) => { + const onOpen = () => { + ws?.removeEventListener("open", onOpen); + ws?.removeEventListener("error", onError); + resolve(); + }; + + const onError = (e: Event) => { + ws?.removeEventListener("open", onOpen); + ws?.removeEventListener("error", onError); + reject(e); + }; + + ws?.addEventListener("open", onOpen); + ws?.addEventListener("error", onError); + }); +} From 6a7ea0cd8d90686b08237b082baeb88635216457 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 9 Jan 2026 18:52:22 +0100 Subject: [PATCH 3/8] Updating node structure --- src/app/chat/[id]/page.tsx | 2 +- src/app/server/[ip]/page.tsx | 2 +- src/components/app/AppLayout.tsx | 2 +- src/hooks/use-node.ts | 17 +++ src/lib/auth.ts | 186 ------------------------------- src/lib/node.ts | 171 ++++++++++++++++++++++++++++ 6 files changed, 191 insertions(+), 189 deletions(-) create mode 100644 src/hooks/use-node.ts delete mode 100644 src/lib/auth.ts create mode 100644 src/lib/node.ts diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index 1920354..6816c58 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -5,7 +5,7 @@ import AppLayout from "@/components/app/AppLayout"; import { useParams, useRouter } from "next/navigation"; import useApp from "@/hooks/use-app"; import { useEffect, useRef, useState } from "react"; -import auth, { waitForOpen } from "@/lib/auth"; +import { NodeClient } from "@/lib/node"; import useAsync from "@/hooks/use-async"; import { Button } from "@/components/ui/button"; import useIndicators from "@/hooks/use-indicators"; diff --git a/src/app/server/[ip]/page.tsx b/src/app/server/[ip]/page.tsx index 9e3b012..366c5fc 100644 --- a/src/app/server/[ip]/page.tsx +++ b/src/app/server/[ip]/page.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react"; import { useParams, useSearchParams } from "next/navigation"; import AppLayout from "@/components/app/AppLayout"; -import auth from "@/lib/auth"; +import auth from "@/lib/node"; import useMessages from "@/hooks/use-messages"; import useApp from "@/hooks/use-app"; import { useEffectOnceWhenReady } from "@/hooks/use-once"; diff --git a/src/components/app/AppLayout.tsx b/src/components/app/AppLayout.tsx index 4717a75..18ab178 100644 --- a/src/components/app/AppLayout.tsx +++ b/src/components/app/AppLayout.tsx @@ -4,7 +4,7 @@ import React, { useEffect } from "react"; import { cn } from "@/lib/utils"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; -import auth from "@/lib/auth"; +import auth from "@/lib/node"; import { useIsMobile } from "@/hooks/is-mobile"; import App from "@/types/app"; import { Message } from "@/types/types"; diff --git a/src/hooks/use-node.ts b/src/hooks/use-node.ts new file mode 100644 index 0000000..5927384 --- /dev/null +++ b/src/hooks/use-node.ts @@ -0,0 +1,17 @@ +import { useEffect } from "react"; +import { MessageStore } from "./use-messages"; +import { NodeCallbacks, NodeClient } from "@/lib/node"; + +export default function useNode( + nodeId: string, + messageStore: MessageStore, + callbacks: NodeCallbacks +) { + useEffect(() => { + const client = new NodeClient(nodeId, messageStore, callbacks); + + client.connect(); + + return () => client.disconnect(); + }, [nodeId]); +} diff --git a/src/lib/auth.ts b/src/lib/auth.ts deleted file mode 100644 index 155c4b5..0000000 --- a/src/lib/auth.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { Message, Server } from "@/types/types"; -import { Dispatch, RefObject, SetStateAction } from "react"; -import { toast } from "sonner"; -import { MessageStore } from "@/hooks/use-messages"; -import { IndicatorContext, ServerMessage } from "@/types/protocol"; -import { authRelay } from "@/actions/auth"; -import { getServerById } from "@/actions/get-server"; -import { playPCM16 } from "./audio"; - -type AuthParams = { - id: string; - wsRef: RefObject; - setServer?: Dispatch>; - onNewMessage?: (m: Message) => void; - 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; - onSpeaking?: (voiceId: number) => void; -}; - -export default async function auth({ - id, - wsRef, - setServer, - onNewMessage, - messageStore, - onIndicator, - onVoiceJoin, - onVoiceLeave, - onSpeaking, -}: AuthParams) { - console.log("Authenticating with server id:", id); - - if (!id) return; - - const ip = (await getServerById(id)).address; - - console.log("Authenticating with server at:", ip); - const server_auth = await authRelay(id); - - var name = id; - - // Open the WebSocket connection - const ws = new WebSocket(`wss://${ip}`); - wsRef.current = ws; - - ws.onopen = () => { - console.log("Connected to WebSocket:", ip); - }; - - ws.onmessage = async (m) => { - if (m.data instanceof Blob) { - const buffer = await m.data.arrayBuffer(); - const bytes = new Uint8Array(buffer); - playPCM16(bytes); - if (onSpeaking) onSpeaking(new DataView(buffer).getUint16(0, true)); - return; - } - - var data = JSON.parse(m.data); - console.log("Message received:", data); - - if (data.error) { - toast.error(`Error from ${name} (${data.error})`, { - description: data.message, - }); - return; - } - - if (data.version) { - if (setServer) setServer({ channels: [], ...data }); - ws.send( - JSON.stringify({ - version: "0.0.1", - auth_token: server_auth, - }) - ); - name = data.name; - return; - } - - const msg = data as ServerMessage; - - switch (msg.type) { - case "authenticated": - 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": - if (onNewMessage) onNewMessage(msg.params as Message); - messageStore.addMessage(msg.params as Message); - break; - - case "message_delete": - messageStore.deleteMessage(msg.params.message_id); - break; - - case "message_update": - messageStore.editMessage(msg.params.message_id, msg.params.contents); - break; - case "chunk": - messageStore.insertMessages(msg.params); - break; - case "indicator": - 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; - } - }; - - ws.onerror = (err) => { - console.error("WebSocket error:", err); - - let description = "An unknown WebSocket error occurred."; - - if (err instanceof ErrorEvent) { - // Browser WebSocket errors show up here - description = - err.message || err.error?.message || "WebSocket connection failed."; - } else if (err instanceof Event) { - // Generic Event (no info) - description = "WebSocket encountered a network issue."; - } else if (err && typeof err === "object") { - // Custom data if something special got passed - description = JSON.stringify(err, null, 2); - } else { - description = String(err); - } - - toast.error(`WebSocket error (${name})`, { description }); - }; - - ws.onclose = () => { - console.log("WebSocket closed: ", ip); - }; - - // Cleanup on unmount or id change - return () => { - ws.close(); - }; -} - -export async function waitForOpen(ws: WebSocket | null): Promise { - if (ws?.readyState === WebSocket.OPEN) return Promise.resolve(); - - return new Promise((resolve, reject) => { - const onOpen = () => { - ws?.removeEventListener("open", onOpen); - ws?.removeEventListener("error", onError); - resolve(); - }; - - const onError = (e: Event) => { - ws?.removeEventListener("open", onOpen); - ws?.removeEventListener("error", onError); - reject(e); - }; - - ws?.addEventListener("open", onOpen); - ws?.addEventListener("error", onError); - }); -} diff --git a/src/lib/node.ts b/src/lib/node.ts new file mode 100644 index 0000000..4449643 --- /dev/null +++ b/src/lib/node.ts @@ -0,0 +1,171 @@ +import { Message, Server } from "@/types/types"; +import { toast } from "sonner"; +import { MessageStore } from "@/hooks/use-messages"; +import { IndicatorContext, ServerMessage } from "@/types/protocol"; +import { authRelay } from "@/actions/auth"; +import { getServerById } from "@/actions/get-server"; +import { playPCM16 } from "./audio"; + +export interface NodeCallbacks { + setServer?: (s: Server) => void; + onNewMessage?: (m: Message) => void; + onIndicator?: (i: IndicatorContext) => void; + onVoiceJoin?: (userId: string, channelId: string, voiceId: number) => void; + onVoiceLeave?: (userId: string, channelId: string, voiceId: number) => void; + onSpeaking?: (voiceId: number) => void; +}; + +export class NodeClient { + private ws: WebSocket | null = null; + private name: string; + private readonly serverId: string; + private readonly messageStore: MessageStore; + private readonly callbacks: NodeCallbacks; + + constructor( + serverId: string, + messageStore: MessageStore, + callbacks: NodeCallbacks = {} + ) { + this.serverId = serverId; + this.messageStore = messageStore; + this.callbacks = callbacks; + this.name = serverId; + } + + get socket() { + return this.ws; + } + + async connect() { + if (!this.serverId) return; + + const server = await getServerById(this.serverId); + const authToken = await authRelay(this.serverId); + + this.ws = new WebSocket(`wss://${server.address}`); + + this.ws.onopen = () => { + console.log("WebSocket connected:", server.address); + }; + + this.ws.onmessage = (e) => this.handleMessage(e, authToken); + this.ws.onerror = (e) => this.handleError(e); + this.ws.onclose = () => { + console.log("WebSocket closed:", server.address); + }; + } + + disconnect() { + this.ws?.close(); + this.ws = null; + } + + private async handleMessage(e: MessageEvent, authToken: string) { + // Voice packet + if (e.data instanceof Blob) { + const buffer = await e.data.arrayBuffer(); + const bytes = new Uint8Array(buffer); + + playPCM16(bytes); + + if (this.callbacks.onSpeaking) { + const voiceId = new DataView(buffer).getUint16(0, true); + this.callbacks.onSpeaking(voiceId); + } + return; + } + + const data = JSON.parse(e.data); + + if (data.error) { + toast.error(`Error from ${this.name} (${data.error})`, { + description: data.message, + }); + return; + } + + // Handshake + if (data.version) { + this.callbacks.setServer?.({ channels: [], ...data }); + + this.ws?.send( + JSON.stringify({ + version: "0.0.1", + auth_token: authToken, + }) + ); + + this.name = data.name; + return; + } + + this.handleServerMessage(data as ServerMessage); + } + + private handleServerMessage(msg: ServerMessage) { + switch (msg.type) { + case "authenticated": + msg.params.indicators.forEach((i) => this.callbacks.onIndicator?.(i)); + + Object.entries(msg.params.voice_chat).forEach(([channelId, users]) => + Object.entries(users).forEach(([userId, voiceId]) => + this.callbacks.onVoiceJoin?.(userId, channelId, voiceId) + ) + ); + break; + + case "message_create": + this.callbacks.onNewMessage?.(msg.params); + this.messageStore.addMessage(msg.params); + break; + + case "message_delete": + this.messageStore.deleteMessage(msg.params.message_id); + break; + + case "message_update": + this.messageStore.editMessage( + msg.params.message_id, + msg.params.contents + ); + break; + + case "chunk": + this.messageStore.insertMessages(msg.params); + break; + + case "indicator": + this.callbacks.onIndicator?.(msg.params); + break; + + case "voice_join": + this.callbacks.onVoiceJoin?.( + msg.params.user_id, + msg.params.channel_id, + msg.params.voice_id + ); + break; + + case "voice_leave": + this.callbacks.onVoiceLeave?.( + msg.params.user_id, + msg.params.channel_id, + msg.params.voice_id + ); + break; + } + } + + private handleError(err: Event) { + console.error("WebSocket error:", err); + + let description = "WebSocket connection failed."; + + if (err instanceof ErrorEvent) { + description = err.message || description; + } + + toast.error(`WebSocket error (${this.name})`, { description }); + } +} From 5c4526e578ac6f2723f0216a87ab5d5d94ccebc3 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 9 Jan 2026 19:56:44 +0100 Subject: [PATCH 4/8] Updated structure --- src/app/chat/[id]/page.tsx | 57 +++++++++-------- src/app/server/[id]/page.tsx | 68 ++++++++++++++++++++ src/app/server/[ip]/page.tsx | 81 ------------------------ src/components/app/AppLayout.tsx | 56 ++++++++-------- src/components/channel/ChannelView.tsx | 39 ++++++------ src/components/channel/PluginChannel.tsx | 7 +- src/hooks/use-app.ts | 3 +- src/hooks/use-node.ts | 40 ++++++++++-- src/hooks/use-once.ts | 5 +- src/lib/node.ts | 5 +- src/types/app.ts | 5 +- 11 files changed, 197 insertions(+), 169 deletions(-) create mode 100644 src/app/server/[id]/page.tsx delete mode 100644 src/app/server/[ip]/page.tsx diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index 6816c58..bcbda61 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -4,65 +4,68 @@ import MessageBox from "@/components/channel/MessageBox"; import AppLayout from "@/components/app/AppLayout"; import { useParams, useRouter } from "next/navigation"; import useApp from "@/hooks/use-app"; -import { useEffect, useRef, useState } from "react"; -import { NodeClient } from "@/lib/node"; +import { useEffect } from "react"; import useAsync from "@/hooks/use-async"; import { Button } from "@/components/ui/button"; import useIndicators from "@/hooks/use-indicators"; -import { useEffectOnceWhenReady } from "@/hooks/use-once"; import { ClientMessage } from "@/types/protocol"; +import { getNode, useNullNode } from "@/hooks/use-node"; +import { useEffectOnceWhenReady } from "@/hooks/use-once"; export default function DMs() { const { id } = useParams<{ id: string }>(); const app = useApp(); - const targetNode = useRef(null); const { value: target, loading } = useAsync(() => app.getUserById(id)); const router = useRouter(); const { indicators, addIndicator } = useIndicators(); const sendRequest = async (req: ClientMessage) => { - console.log("Sending request: ", req); - await waitForOpen(targetNode.current); - await waitForOpen(app.node.current); + console.log("Sending request:", req); switch (req.type) { case "send_message": - targetNode.current?.send(JSON.stringify(req)); + targetNode.current?.socket?.send(JSON.stringify(req)); + if (target?.node_address !== app.profile?.node_address) + app.node.current?.socket?.send(JSON.stringify(req)); break; case "load_chunk": - targetNode.current?.send(JSON.stringify(req)); - app.node.current?.send(JSON.stringify(req)); + targetNode.current?.socket?.send(JSON.stringify(req)); + if (target?.node_address !== app.profile?.node_address) + app.node.current?.socket?.send(JSON.stringify(req)); break; default: - app.node.current?.send(JSON.stringify(req)); + app.node.current?.socket?.send(JSON.stringify(req)); break; } }; + const targetNode = useNullNode(); + useEffectOnceWhenReady( () => { - async function connectTargetNode() { - if (!target) return; - await auth({ - id: target.node_address, - wsRef: targetNode, - messageStore: app.privateMessages, - onIndicator: addIndicator, - }); - } + if (!target) return; - connectTargetNode().then(() => { - app.setCurrentChannel({ - id: id, - name: target?.display_name || id, - kind: "text", - }); - }); + return getNode( + target?.node_address, + app.privateMessages, + { + onIndicator: addIndicator, + }, + targetNode + ); }, [target], [(v) => v] ); + useEffect(() => { + app.setCurrentChannel({ + id: id, + name: target?.display_name || id, + kind: "text", + }); + }, [targetNode]); + return loading || target ? ( (); + const searchParams = useSearchParams(); + const app = useApp(); + const messageStore = useMessages(); + const { indicators, addIndicator } = useIndicators(); + + const serverNode = useNode(id, messageStore, { + setServer: app.setServer, + 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, + }; + }), + onSpeaking: (voiceId) => + app.setSpeaking((speaking) => ({ + ...speaking, + [voiceId]: Date.now(), + })), + }); + + useEffect(() => { + const channelId = searchParams.get("ch"); + if (channelId) { + app.setCurrentChannel( + app.server?.channels.find((v) => v.id === channelId) || + app.server?.channels[0] || + null + ); + } else { + app.setCurrentChannel(app.server?.channels[0] || null); + } + }, [app.server]); + + return ( + + m.channel_id === app.currentChannel?.id + )} + ip={id} + indicators={indicators} + /> + + ); +} diff --git a/src/app/server/[ip]/page.tsx b/src/app/server/[ip]/page.tsx deleted file mode 100644 index 366c5fc..0000000 --- a/src/app/server/[ip]/page.tsx +++ /dev/null @@ -1,81 +0,0 @@ -"use client"; - -import { useEffect, useRef, useState } from "react"; -import { useParams, useSearchParams } from "next/navigation"; -import AppLayout from "@/components/app/AppLayout"; -import auth from "@/lib/node"; -import useMessages from "@/hooks/use-messages"; -import useApp from "@/hooks/use-app"; -import { useEffectOnceWhenReady } from "@/hooks/use-once"; -import ChannelView from "@/components/channel/ChannelView"; -import { IndicatorContext } from "@/types/protocol"; -import useIndicators from "@/hooks/use-indicators"; - -export default function Server() { - const { ip } = useParams<{ ip: string }>(); - const searchParams = useSearchParams(); - const serverRef = useRef(null); - const app = useApp(); - const messageStore = useMessages(); - const { indicators, addIndicator } = useIndicators(); - - useEffectOnceWhenReady( - () => { - if (!ip) return; - auth({ - id: ip, - wsRef: serverRef, - 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, - }; - }), - onSpeaking: (voiceId) => - app.setSpeaking((speaking) => ({ - ...speaking, - [voiceId]: Date.now(), - })), - }); - }, - [ip, messageStore.messages], - [undefined, (v) => v] - ); - - useEffect(() => { - const channelId = searchParams.get("ch"); - if (channelId) { - app.setCurrentChannel( - app.server?.channels.find((v) => v.id === channelId) || - app.server?.channels[0] || - null - ); - } else { - app.setCurrentChannel(app.server?.channels[0] || null); - } - }, [app.server]); - - return ( - - m.channel_id === app.currentChannel?.id - )} - ip={ip} - serverRef={serverRef} - indicators={indicators} - /> - - ); -} diff --git a/src/components/app/AppLayout.tsx b/src/components/app/AppLayout.tsx index 18ab178..fec5a8d 100644 --- a/src/components/app/AppLayout.tsx +++ b/src/components/app/AppLayout.tsx @@ -4,7 +4,6 @@ import React, { useEffect } from "react"; import { cn } from "@/lib/utils"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; -import auth from "@/lib/node"; import { useIsMobile } from "@/hooks/is-mobile"; import App from "@/types/app"; import { Message } from "@/types/types"; @@ -14,6 +13,7 @@ import SidebarDMs from "./SidebarDMs"; import SidebarChannels from "./SidebarChannels"; import DMItem from "./DMItem"; import { useEffectOnceWhenReady } from "@/hooks/use-once"; +import { getNode, useNullNode } from "@/hooks/use-node"; export default function AppLayout({ children, @@ -28,44 +28,46 @@ export default function AppLayout({ }) { const router = useRouter(); const isMobile = useIsMobile(); + const node = useNullNode(); useEffectOnceWhenReady( () => { if (!app.profile?.node_address) return; - auth({ - id: app.profile.node_address, - wsRef: app.node, - onNewMessage: (m) => { - if (m.from !== chatWith && m.from !== app.profile?.id) - toast( - `New message from ${ - app.profiles[m.from]?.display_name || m.from - }`, - { - description: m.contents.slice(0, 80), - action: { - label: "View", - onClick: () => router.push(`/chat/${m.from}`), - }, - position: "top-right", - } - ); + return getNode( + app.profile?.node_address, + app.privateMessages, + { + onNewMessage: (m) => { + if (m.from !== chatWith && m.from !== app.profile?.id) + toast( + `New message from ${ + app.profiles[m.from]?.display_name || m.from + }`, + { + description: m.contents.slice(0, 80), + action: { + label: "View", + onClick: () => router.push(`/chat/${m.from}`), + }, + position: "top-right", + } + ); - if (onNewMessage) onNewMessage(m); + if (onNewMessage) onNewMessage(m); + }, }, - messageStore: app.privateMessages, - }); - - app.setProfiles((prev) => { - if (app.profile) prev[app.profile.id] = app.profile; - return prev; - }); + node + ); }, [chatWith, app.profile], [undefined, undefined] ); + useEffect(() => { + app.node.current = node.current; + }, [node]); + useEffect(() => app.setSidebarOpen(!isMobile), [isMobile]); return ( diff --git a/src/components/channel/ChannelView.tsx b/src/components/channel/ChannelView.tsx index 0fb7d6b..bdad0ee 100644 --- a/src/components/channel/ChannelView.tsx +++ b/src/components/channel/ChannelView.tsx @@ -1,29 +1,36 @@ import { Message } from "@/types/types"; import MessageBox from "./MessageBox"; import App from "@/types/app"; -import React from "react"; +import React, { RefObject } from "react"; import PluginChannel from "./PluginChannel"; -import { IndicatorContext } from "@/types/protocol"; +import { ClientMessage, IndicatorContext } from "@/types/protocol"; import VoiceBox from "./voice/VoiceBox"; import { sendVoice } from "@/lib/audio"; +import { NodeClient } from "@/lib/node"; export default function ChannelView({ app, messages, ip, - serverRef, + node, indicators, }: { app: App; messages: Message[]; ip: string; - serverRef: React.RefObject; indicators: IndicatorContext[]; + node: RefObject; }) { if (!app.currentChannel) return (
); + + const sendRequest = (req: ClientMessage) => { + node.current?.socket?.send(JSON.stringify(req)); + console.log(node.current?.socket); + }; + switch (app.currentChannel.kind) { case "text": return ( @@ -31,10 +38,7 @@ export default function ChannelView({ app={app} channelName={app.currentChannel.name} messages={messages} - sendRequest={(req) => { - while (serverRef.current?.CONNECTING) {} - serverRef.current?.send(JSON.stringify(req)); - }} + sendRequest={sendRequest} indicators={indicators.filter( (indicator) => indicator.indicator.params.user_id !== app.profile?.id @@ -49,13 +53,13 @@ export default function ChannelView({ channelName={app.currentChannel.name} channelId={app.currentChannel.id} messages={messages} - sendRequest={(req) => { - while (serverRef.current?.CONNECTING) {} - serverRef.current?.send(JSON.stringify(req)); - }} + sendRequest={sendRequest} sendVoice={(data) => { - while (!serverRef.current || serverRef.current?.CONNECTING) {} - sendVoice(serverRef.current, data); + if (!node.current?.socket) { + return; + } + + sendVoice(node.current?.socket, data); }} indicators={indicators.filter( (indicator) => @@ -66,12 +70,7 @@ export default function ChannelView({ default: return ( - + ); } } diff --git a/src/components/channel/PluginChannel.tsx b/src/components/channel/PluginChannel.tsx index a1fead5..465ac1d 100644 --- a/src/components/channel/PluginChannel.tsx +++ b/src/components/channel/PluginChannel.tsx @@ -2,17 +2,18 @@ import { Message } from "@/types/types"; import App from "@/types/app"; import React, { useEffect, useRef } from "react"; import { useParentCommunication } from "react-iframes-bridge"; +import { NodeClient } from "@/lib/node"; export default function PluginChannel({ app, messages, ip, - serverRef, + node, }: { app: App; messages: Message[]; ip: string; - serverRef: React.RefObject; + node: React.RefObject; }) { if (!app.currentChannel) return ( @@ -32,7 +33,7 @@ export default function PluginChannel({ }); parentComm.onMessage("SEND_MESSAGE", (msg: string) => { - serverRef.current?.send( + node.current?.socket?.send( JSON.stringify({ type: "send_message", params: { diff --git a/src/hooks/use-app.ts b/src/hooks/use-app.ts index 9e71dca..dff41a5 100644 --- a/src/hooks/use-app.ts +++ b/src/hooks/use-app.ts @@ -9,9 +9,10 @@ import Cookies from "js-cookie"; import { toast } from "sonner"; import { getProfileById } from "@/actions/get-profile"; import { getServerById } from "@/actions/get-server"; +import { NodeClient } from "@/lib/node"; export default function useApp(): App { - const node = useRef(null); + const node = useRef(null); const [profiles, setProfiles] = useState>({}); const [sidebarOpen, setSidebarOpen] = useState(false); const profile = useUser(); diff --git a/src/hooks/use-node.ts b/src/hooks/use-node.ts index 5927384..9738daf 100644 --- a/src/hooks/use-node.ts +++ b/src/hooks/use-node.ts @@ -1,17 +1,47 @@ -import { useEffect } from "react"; +import { RefObject, useEffect, useRef } from "react"; import { MessageStore } from "./use-messages"; import { NodeCallbacks, NodeClient } from "@/lib/node"; +import { asyncWrapProviders } from "async_hooks"; export default function useNode( nodeId: string, messageStore: MessageStore, callbacks: NodeCallbacks ) { + const wsClientRef = useRef(null); + useEffect(() => getNode(nodeId, messageStore, callbacks, wsClientRef), []); + return wsClientRef; +} + +export function useDynNode( + nodeId: string | undefined | null, + messageStore: MessageStore, + callbacks: NodeCallbacks +) { + const wsClientRef = useRef(null); + useEffect(() => { - const client = new NodeClient(nodeId, messageStore, callbacks); + if (!nodeId) return; + return getNode(nodeId, messageStore, callbacks, wsClientRef); + }, [nodeId]); + + return wsClientRef; +} - client.connect(); +export function getNode( + nodeId: string, + messageStore: MessageStore, + callbacks: NodeCallbacks, + ref: RefObject +) { + const client = new NodeClient(nodeId, messageStore, callbacks); - return () => client.disconnect(); - }, [nodeId]); + ref.current = client; + client.connect(); + + return () => client.disconnect(); +} + +export function useNullNode() { + return useRef(null); } diff --git a/src/hooks/use-once.ts b/src/hooks/use-once.ts index 4dcc853..051c629 100644 --- a/src/hooks/use-once.ts +++ b/src/hooks/use-once.ts @@ -1,7 +1,7 @@ import { useEffect, useRef } from "react"; export function useEffectOnceWhenReady( - effect: () => void, + effect: () => any, deps: any[], testers: (undefined | ((m: any) => boolean))[] ) { @@ -13,6 +13,7 @@ export function useEffectOnceWhenReady( ); if (!allReady || hasRun.current) return; hasRun.current = true; - effect(); + + return effect(); }, deps); } diff --git a/src/lib/node.ts b/src/lib/node.ts index 4449643..8b10c18 100644 --- a/src/lib/node.ts +++ b/src/lib/node.ts @@ -13,7 +13,7 @@ export interface NodeCallbacks { onVoiceJoin?: (userId: string, channelId: string, voiceId: number) => void; onVoiceLeave?: (userId: string, channelId: string, voiceId: number) => void; onSpeaking?: (voiceId: number) => void; -}; +} export class NodeClient { private ws: WebSocket | null = null; @@ -57,6 +57,7 @@ export class NodeClient { } disconnect() { + console.log("Disconnecting"); this.ws?.close(); this.ws = null; } @@ -78,6 +79,8 @@ export class NodeClient { const data = JSON.parse(e.data); + console.log("Message received:", data); + if (data.error) { toast.error(`Error from ${this.name} (${data.error})`, { description: data.message, diff --git a/src/types/app.ts b/src/types/app.ts index b9304f2..c9feb74 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -1,14 +1,15 @@ import { Dispatch, RefObject, SetStateAction } from "react"; import { StringMap } from "./typeUtils"; import { UserProfile } from "@/hooks/get-user"; -import { Channel, Message, Server } from "./types"; +import { Channel, Server } from "./types"; import { ClientSettings } from "./settings"; import { MessageStore } from "@/hooks/use-messages"; +import { NodeClient } from "@/lib/node"; export type SetState = Dispatch>; export default interface App { - node: RefObject; + node: RefObject; profile: UserProfile | null; server: Server | undefined; From 11fba61b60e16c172c74170f4244edd36fbfaa83 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 9 Jan 2026 20:10:35 +0100 Subject: [PATCH 5/8] Fixed a few things --- src/app/chat/[id]/page.tsx | 3 +++ src/components/channel/ChannelView.tsx | 3 ++- src/components/channel/PluginChannel.tsx | 3 ++- src/lib/node.ts | 15 +++++++++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index bcbda61..457f80d 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -22,6 +22,9 @@ export default function DMs() { const sendRequest = async (req: ClientMessage) => { console.log("Sending request:", req); + await targetNode.current?.waitUntilReady(); + await app.node.current?.waitUntilReady(); + switch (req.type) { case "send_message": targetNode.current?.socket?.send(JSON.stringify(req)); diff --git a/src/components/channel/ChannelView.tsx b/src/components/channel/ChannelView.tsx index bdad0ee..c019d6c 100644 --- a/src/components/channel/ChannelView.tsx +++ b/src/components/channel/ChannelView.tsx @@ -26,7 +26,8 @@ export default function ChannelView({
); - const sendRequest = (req: ClientMessage) => { + const sendRequest = async (req: ClientMessage) => { + await node.current?.waitUntilReady(); node.current?.socket?.send(JSON.stringify(req)); console.log(node.current?.socket); }; diff --git a/src/components/channel/PluginChannel.tsx b/src/components/channel/PluginChannel.tsx index 465ac1d..afbf9bc 100644 --- a/src/components/channel/PluginChannel.tsx +++ b/src/components/channel/PluginChannel.tsx @@ -32,7 +32,8 @@ export default function PluginChannel({ parentComm.sendToChild("MESSAGES", messages); }); - parentComm.onMessage("SEND_MESSAGE", (msg: string) => { + parentComm.onMessage("SEND_MESSAGE", async (msg: string) => { + await node.current?.waitUntilReady(); node.current?.socket?.send( JSON.stringify({ type: "send_message", diff --git a/src/lib/node.ts b/src/lib/node.ts index 8b10c18..d5aadcd 100644 --- a/src/lib/node.ts +++ b/src/lib/node.ts @@ -21,6 +21,9 @@ export class NodeClient { private readonly serverId: string; private readonly messageStore: MessageStore; private readonly callbacks: NodeCallbacks; + private ready: boolean; + private readyPromise: Promise; + private resolveReady!: () => void; constructor( serverId: string, @@ -31,6 +34,10 @@ export class NodeClient { this.messageStore = messageStore; this.callbacks = callbacks; this.name = serverId; + this.ready = false; + this.readyPromise = new Promise((resolve) => { + this.resolveReady = resolve; + }); } get socket() { @@ -62,6 +69,11 @@ export class NodeClient { this.ws = null; } + async waitUntilReady() { + if (this.ready) return; + await this.readyPromise; + } + private async handleMessage(e: MessageEvent, authToken: string) { // Voice packet if (e.data instanceof Blob) { @@ -116,6 +128,9 @@ export class NodeClient { this.callbacks.onVoiceJoin?.(userId, channelId, voiceId) ) ); + + this.ready = true; + this.resolveReady(); break; case "message_create": From 8df66a2a94d106276fb3bf33ac8ad9f19716e328 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 9 Jan 2026 20:26:05 +0100 Subject: [PATCH 6/8] Private message caching --- src/app/auth/CreateAccount.tsx | 1 + src/app/auth/Login.tsx | 1 + src/hooks/use-app.ts | 17 +++++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/src/app/auth/CreateAccount.tsx b/src/app/auth/CreateAccount.tsx index 8424e67..2e35eff 100644 --- a/src/app/auth/CreateAccount.tsx +++ b/src/app/auth/CreateAccount.tsx @@ -83,6 +83,7 @@ export default function CreateAccount() { Cookies.set("verify_temp_password", password); router.push(`/verify`); Cookies.set("servers", ""); + localStorage.clear(); } else { setFeedback({ message: response.message, kind: "error" }); } diff --git a/src/app/auth/Login.tsx b/src/app/auth/Login.tsx index 31e8a2e..cd6265f 100644 --- a/src/app/auth/Login.tsx +++ b/src/app/auth/Login.tsx @@ -30,6 +30,7 @@ export default function Login() { }); Cookies.set("servers", ""); + localStorage.clear(); router.push("/chat"); } catch (err: any) { setFeedback({ diff --git a/src/hooks/use-app.ts b/src/hooks/use-app.ts index dff41a5..87ce7b0 100644 --- a/src/hooks/use-app.ts +++ b/src/hooks/use-app.ts @@ -27,6 +27,7 @@ export default function useApp(): App { >({}); const [speaking, setSpeaking] = useState>({}); + const [pmsLoaded, setPmsLoaded] = useState(false); useEffect(() => { const stored = Cookies.get("servers"); @@ -42,6 +43,22 @@ export default function useApp(): App { .catch(toast.error); }, []); + useEffect(() => { + const json = localStorage.getItem("privateMessages"); + if (json) { + privateMessages.setMessages(JSON.parse(json)); + } + setPmsLoaded(true); + }, []); + + useEffect(() => { + if (!pmsLoaded) return; + localStorage.setItem( + "privateMessages", + JSON.stringify(privateMessages.messages) + ); + }, [privateMessages.messages, pmsLoaded]); + return { speaking, setSpeaking, From dad1e3f04a344fd0ae63227d0d95dceab00bfa96 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 9 Jan 2026 20:50:45 +0100 Subject: [PATCH 7/8] Message loading --- src/app/chat/[id]/page.tsx | 1 + src/components/app/AppLayout.tsx | 3 +++ src/lib/node.ts | 14 ++++++++++++++ 3 files changed, 18 insertions(+) diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index 457f80d..e530030 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -53,6 +53,7 @@ export default function DMs() { app.privateMessages, { onIndicator: addIndicator, + setServer: () => targetNode.current?.loadInitialMessages(id), }, targetNode ); diff --git a/src/components/app/AppLayout.tsx b/src/components/app/AppLayout.tsx index fec5a8d..1180602 100644 --- a/src/components/app/AppLayout.tsx +++ b/src/components/app/AppLayout.tsx @@ -14,6 +14,7 @@ import SidebarChannels from "./SidebarChannels"; import DMItem from "./DMItem"; import { useEffectOnceWhenReady } from "@/hooks/use-once"; import { getNode, useNullNode } from "@/hooks/use-node"; +import { ClientMessage } from "@/types/protocol"; export default function AppLayout({ children, @@ -56,6 +57,8 @@ export default function AppLayout({ if (onNewMessage) onNewMessage(m); }, + setServer: () => + chatWith && app.node.current?.loadInitialMessages(chatWith), }, node ); diff --git a/src/lib/node.ts b/src/lib/node.ts index d5aadcd..b9ca04d 100644 --- a/src/lib/node.ts +++ b/src/lib/node.ts @@ -74,6 +74,20 @@ export class NodeClient { await this.readyPromise; } + loadInitialMessages(channelId: string) { + this.waitUntilReady().then(() => { + this.socket?.send( + JSON.stringify({ + type: "load_chunk", + params: { + channel_id: channelId, + chunk_id: 0, + }, + }) + ); + }); + } + private async handleMessage(e: MessageEvent, authToken: string) { // Voice packet if (e.data instanceof Blob) { From 5635f483e4ad7d1413eb85242981d8efff45f1b8 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 9 Jan 2026 20:59:12 +0100 Subject: [PATCH 8/8] Fixed message dupes --- src/hooks/use-app.ts | 17 ------------- src/hooks/use-messages.ts | 52 ++++++++++++++++++++++++++++----------- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/hooks/use-app.ts b/src/hooks/use-app.ts index 87ce7b0..dff41a5 100644 --- a/src/hooks/use-app.ts +++ b/src/hooks/use-app.ts @@ -27,7 +27,6 @@ export default function useApp(): App { >({}); const [speaking, setSpeaking] = useState>({}); - const [pmsLoaded, setPmsLoaded] = useState(false); useEffect(() => { const stored = Cookies.get("servers"); @@ -43,22 +42,6 @@ export default function useApp(): App { .catch(toast.error); }, []); - useEffect(() => { - const json = localStorage.getItem("privateMessages"); - if (json) { - privateMessages.setMessages(JSON.parse(json)); - } - setPmsLoaded(true); - }, []); - - useEffect(() => { - if (!pmsLoaded) return; - localStorage.setItem( - "privateMessages", - JSON.stringify(privateMessages.messages) - ); - }, [privateMessages.messages, pmsLoaded]); - return { speaking, setSpeaking, diff --git a/src/hooks/use-messages.ts b/src/hooks/use-messages.ts index 883d84a..fce9bec 100644 --- a/src/hooks/use-messages.ts +++ b/src/hooks/use-messages.ts @@ -10,23 +10,45 @@ export interface MessageStore { clearMessages: () => void; insertMessages: (msgs: Message[]) => void; } + export default function useMessages(): MessageStore { - const [messages, setMessages] = useState([]); + const [messages, setMessagesState] = useState([]); + + // Helper to ensure unique messages by id + const uniqueMessages = (msgs: Message[]) => { + const map = new Map(); + msgs.forEach((m) => map.set(m.id, m)); // duplicates overwrite + return Array.from(map.values()); + }; + + const setMessages = (msgs: Message[]) => + setMessagesState(uniqueMessages(msgs)); + + const addMessage = (msg: Message) => + setMessagesState((messages) => + messages.some((m) => m.id === msg.id) ? messages : [...messages, msg] + ); + + const insertMessages = (msgs: Message[]) => + setMessagesState((messages) => uniqueMessages([...msgs, ...messages])); + + const deleteMessage = (id: number) => + setMessagesState((messages) => messages.filter((m) => m.id !== id)); + + const editMessage = (id: number, contents: string) => + setMessagesState((messages) => + messages.map((m) => (m.id === id ? { ...m, contents } : m)) + ); + + const clearMessages = () => setMessagesState([]); return { - messages: messages, - setMessages: (msgs) => setMessages(msgs), - addMessage: (msg) => - setMessages((messages) => - messages.some((m) => m.id === msg.id) ? messages : [...messages, msg] - ), - deleteMessage: (id: number) => - setMessages((messages) => messages.filter((m) => m.id !== id)), - editMessage: (id: number, contents) => - setMessages((messages) => - messages.map((m) => (m.id === id ? { ...m, contents } : m)) - ), - clearMessages: () => setMessages([]), - insertMessages: (msgs) => setMessages((m) => [...msgs, ...m]), + messages, + setMessages, + addMessage, + deleteMessage, + editMessage, + clearMessages, + insertMessages, }; }