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;