diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx
index aaaf2b7..340d749 100644
--- a/src/app/chat/[id]/page.tsx
+++ b/src/app/chat/[id]/page.tsx
@@ -1,7 +1,7 @@
"use client";
import MessageBox from "@/components/MessageBox";
-import AppSidebar from "@/components/Sidebar";
+import AppLayout from "@/components/app/AppLayout";
import { useParams } from "next/navigation";
import useApp from "@/hooks/use-app";
import { useEffect, useRef } from "react";
@@ -30,7 +30,7 @@ export default function DMs() {
}, [target]);
return (
-
+
-
+
);
}
diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx
index a35b042..600e224 100644
--- a/src/app/chat/page.tsx
+++ b/src/app/chat/page.tsx
@@ -2,7 +2,7 @@
import ProfilePicture from "@/components/ProfilePicture";
import SettingsDialog from "@/components/settings/SettingsDialog";
-import AppSidebar from "@/components/Sidebar";
+import AppLayout from "@/components/app/AppLayout";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
@@ -28,7 +28,7 @@ export default function ChatHub() {
const activeServers: Server[] = [];
return (
-
+
-
+
);
}
diff --git a/src/app/server/[ip]/page.tsx b/src/app/server/[ip]/page.tsx
index 1ad9a43..e847af2 100644
--- a/src/app/server/[ip]/page.tsx
+++ b/src/app/server/[ip]/page.tsx
@@ -3,7 +3,7 @@
import { useRef, useState } from "react";
import { useParams } from "next/navigation";
import MessageBox from "@/components/MessageBox";
-import AppSidebar from "@/components/Sidebar";
+import AppLayout from "@/components/app/AppLayout";
import { Server as ServerType } from "@/types/types";
import auth from "@/lib/auth";
import { useServerMessages } from "@/hooks/use-messages";
@@ -35,7 +35,7 @@ export default function Server() {
);
return (
-
+
-
+
);
}
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
deleted file mode 100644
index 50f66ae..0000000
--- a/src/components/Sidebar.tsx
+++ /dev/null
@@ -1,345 +0,0 @@
-"use client";
-
-import {
- HashIcon,
- MessageCircle,
- Plus,
- Search,
- Volume2Icon,
-} from "lucide-react";
-import { Server, Channel, Message } from "@/types/types";
-import React, { useEffect, useMemo, useState } from "react";
-import Cookies from "js-cookie";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "./ui/dialog";
-import { Input } from "./ui/input";
-import { Label } from "./ui/label";
-import { Button } from "./ui/button";
-import { Card } from "./ui/card";
-import { ScrollArea } from "./ui/scroll-area";
-import { useRouter } from "next/navigation";
-import { UserProfile } from "@/hooks/get-user";
-import ProfilePicture from "./ProfilePicture";
-import auth from "@/lib/auth";
-import { toast } from "sonner";
-import SettingsDialog from "./settings/SettingsDialog";
-import Link from "next/link";
-import { cn } from "@/lib/utils";
-import App from "@/types/app";
-import { ProfileSettings } from "@/types/settings";
-import axios from "axios";
-import { useIsMobile } from "@/hooks/is-mobile";
-import { get } from "@/lib/request";
-
-export default function AppSidebar({
- children,
- chatWith,
- onNewMessage,
- server,
- app,
-}: Readonly<{
- onNewMessage?: (msg: Message) => void;
- chatWith?: string;
- children?: React.ReactNode;
- server?: Server | undefined;
- app: App;
-}>) {
- const [newServer, setNewServer] = useState("");
- const [query, setQuery] = useState("");
- const router = useRouter();
- const isMobile = useIsMobile();
-
- const dms = useMemo(() => {
- return Array.from(
- new Set(
- app.messages
- ?.flatMap((item) => [item.from, item.channel_id])
- .filter((item) => item !== app.profile?.id) || []
- )
- );
- }, [app.messages, app.profile?.id]);
-
- useEffect(() => {
- const load = async () => {
- const users = await Promise.all(dms.map((id) => app.getUserById(id)));
- app.setDms(
- users.filter(
- (user) =>
- user !== null &&
- (user?.username.toLowerCase().includes(query.toLowerCase()) ||
- user?.id.toLowerCase().includes(query.toLowerCase()))
- ) as UserProfile[]
- );
- };
- load();
- }, [dms, query]);
-
- useEffect(() => {}, []);
-
- useEffect(() => {
- if (!app.profile?.node_address) return;
- auth(
- app.profile?.node_address,
- app.node,
- () => {},
- app.addMessage,
- (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);
- },
- app.messages ? app.messages[app.messages.length - 1]?.id : undefined
- );
- app.setProfiles((prev) => {
- if (app.profile) prev[app.profile.id] = app.profile;
- return prev;
- });
- }, [chatWith, app.profile]);
-
- const handleAddServer = async () => {
- if (!newServer || Object.hasOwn(app.servers, newServer)) return;
-
- try {
- const server = (await get(`/api/server/${newServer}`)).data as Server;
- app.setServers((prev) => ({
- ...prev,
- [server.id]: server,
- }));
- Cookies.set("servers", Object.keys(app.servers).join(","));
- setNewServer("");
- } catch (e: any) {
- toast.error(e);
- }
- };
-
- return (
-
-
-
-
-
- {Object.values(app.servers).map(
- (srv) =>
- srv && (
-
-
-
- )
- )}
-
-
-
-
-
-
- {/* Right column */}
-
- {server ? (
-
-
{server.name}
-
- ) : (
-
-
- setQuery(e.target.value)}
- />
-
-
-
- )}
-
-
- {server ? (
- <>
- {server.channels.map((channel) => (
-
- ))}
- >
- ) : (
- <>
- {app.dms.map((user) => (
-
- ))}
- {app.dms.length === 0 && query.trim() !== "" ? (
-
-
-
- ) : null}
- >
- )}
-
-
-
-
-
-
- {app.sidebarOpen && isMobile ? (
-
app.setSidebarOpen(false)}
- >
-
- {children}
-
-
- ) : (
- children
- )}
-
- );
-}
-
-function DMItem({
- name,
- id,
- avatar,
- status,
- settings,
- app,
-}: {
- name: string;
- id: string;
- avatar: string;
- status: string;
- settings?: boolean;
- app: App;
-}) {
- return (
-
-
-
- {name}
- {status}
-
- {settings && (
-
- )}
-
- );
-}
-
-function ChannelProp(channel: Channel) {
- return (
-
-
- {channel.name}
-
- );
-}
-
-function ChannelIcon({ kind }: { kind: "text" | "voice" }) {
- switch (kind) {
- case "text":
- return ;
- case "voice":
- return ;
- default:
- return null;
- }
-}
diff --git a/src/components/app/AppLayout.tsx b/src/components/app/AppLayout.tsx
new file mode 100644
index 0000000..c65f969
--- /dev/null
+++ b/src/components/app/AppLayout.tsx
@@ -0,0 +1,101 @@
+"use client";
+
+import React, { useEffect, useMemo, useState } from "react";
+import { cn } from "@/lib/utils";
+import { useRouter } from "next/navigation";
+import { get } from "@/lib/request";
+import Cookies from "js-cookie";
+import { toast } from "sonner";
+import auth from "@/lib/auth";
+import { useIsMobile } from "@/hooks/is-mobile";
+import App from "@/types/app";
+import { Server, Message } from "@/types/types";
+
+import SidebarServers from "./SidebarServers";
+import SidebarDMs from "./SidebarDMs";
+import SidebarChannels from "./SidebarChannels";
+
+export default function AppLayout({
+ children,
+ chatWith,
+ onNewMessage,
+ server,
+ app,
+}: {
+ onNewMessage?: (msg: Message) => void;
+ chatWith?: string;
+ children?: React.ReactNode;
+ server?: Server;
+ app: App;
+}) {
+ const [query, setQuery] = useState("");
+ const router = useRouter();
+ const isMobile = useIsMobile();
+
+ useEffect(() => {
+ if (!app.profile?.node_address) return;
+
+ auth(
+ app.profile.node_address,
+ app.node,
+ () => {},
+ app.addMessage,
+ (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);
+ },
+ app.messages?.[app.messages.length - 1]?.id
+ );
+
+ app.setProfiles((prev) => {
+ if (app.profile) prev[app.profile.id] = app.profile;
+ return prev;
+ });
+ }, [chatWith, app.profile]);
+
+ return (
+
+
+ {/* Left column — servers */}
+
+
+ {/* Right column — DMs or server channels */}
+ {server ? (
+
+ ) : (
+
+ )}
+
+
+ {app.sidebarOpen && isMobile ? (
+
app.setSidebarOpen(false)}
+ >
+
+ {children}
+
+
+ ) : (
+ children
+ )}
+
+ );
+}
diff --git a/src/components/app/DMItem.tsx b/src/components/app/DMItem.tsx
new file mode 100644
index 0000000..535efbf
--- /dev/null
+++ b/src/components/app/DMItem.tsx
@@ -0,0 +1,43 @@
+"use client";
+
+import Link from "next/link";
+import ProfilePicture from "../ProfilePicture";
+import SettingsDialog from "../settings/SettingsDialog";
+import App from "@/types/app";
+import { ProfileSettings } from "@/types/settings";
+
+export default function DMItem({
+ name,
+ id,
+ avatar,
+ status,
+ settings,
+ app,
+}: {
+ name: string;
+ id: string;
+ avatar: string;
+ status: string;
+ settings?: boolean;
+ app: App;
+}) {
+ return (
+
+
+
+ {name}
+ {status}
+
+ {settings && (
+
+ )}
+
+ );
+}
diff --git a/src/components/app/SidebarChannels.tsx b/src/components/app/SidebarChannels.tsx
new file mode 100644
index 0000000..875740e
--- /dev/null
+++ b/src/components/app/SidebarChannels.tsx
@@ -0,0 +1,42 @@
+"use client";
+import { ScrollArea } from "../ui/scroll-area";
+import { Server, Channel } from "@/types/types";
+import { Card } from "../ui/card";
+import { HashIcon, Volume2Icon } from "lucide-react";
+
+export function ChannelIcon({ kind }: { kind: "text" | "voice" }) {
+ switch (kind) {
+ case "text":
+ return ;
+ case "voice":
+ return ;
+ default:
+ return null;
+ }
+}
+
+export function ChannelItem({ channel }: { channel: Channel }) {
+ return (
+
+
+ {channel.name}
+
+ );
+}
+
+export default function SidebarChannels({ server }: { server: Server }) {
+ return (
+
+
+
{server.name}
+
+
+
+ {server.channels.map((channel: Channel) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/app/SidebarDMs.tsx b/src/components/app/SidebarDMs.tsx
new file mode 100644
index 0000000..fd9832e
--- /dev/null
+++ b/src/components/app/SidebarDMs.tsx
@@ -0,0 +1,105 @@
+"use client";
+
+import { Input } from "../ui/input";
+import { Search, Plus } from "lucide-react";
+import { ScrollArea } from "../ui/scroll-area";
+import { Button } from "../ui/button";
+import { useRouter } from "next/navigation";
+import axios from "axios";
+import DMItem from "./DMItem";
+import App from "@/types/app";
+import { UserProfile } from "@/hooks/get-user";
+import { useEffect } from "react";
+
+export default function SidebarDMs({
+ app,
+ query,
+ setQuery,
+}: {
+ app: App;
+ query: string;
+ setQuery: (v: string) => void;
+}) {
+ const router = useRouter();
+
+ useEffect(() => {
+ const load = async () => {
+ const users = await Promise.all(
+ app.messages
+ .flatMap((m) => [m.from, m.channel_id])
+ .filter((id) => id !== app.profile?.id)
+ .map((id) => app.getUserById(id))
+ );
+ app.setDms(
+ users.filter(
+ (user) =>
+ user &&
+ (user.username.toLowerCase().includes(query.toLowerCase()) ||
+ user.id.toLowerCase().includes(query.toLowerCase()))
+ ) as UserProfile[]
+ );
+ };
+ load();
+ }, [query]);
+
+ return (
+
+
+
+ setQuery(e.target.value)}
+ />
+
+
+
+
+
+
+ {app.dms.map((user) => (
+
+ ))}
+
+ {app.dms.length === 0 && query.trim() !== "" && (
+
+
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/components/app/SidebarServers.tsx b/src/components/app/SidebarServers.tsx
new file mode 100644
index 0000000..3b4b0e8
--- /dev/null
+++ b/src/components/app/SidebarServers.tsx
@@ -0,0 +1,98 @@
+"use client";
+
+import { Plus, MessageCircle } from "lucide-react";
+import Link from "next/link";
+import Cookies from "js-cookie";
+import { Button } from "../ui/button";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "../ui/dialog";
+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";
+import { useRouter } from "next/navigation";
+
+export default function SidebarServers({ app }: { app: App }) {
+ const [newServer, setNewServer] = useState("");
+ const router = useRouter();
+
+ const handleAddServer = async () => {
+ if (!newServer || Object.hasOwn(app.servers, newServer)) return;
+ try {
+ const server = (await get(`/api/server/${newServer}`)).data as Server;
+ app.setServers((prev) => ({ ...prev, [server.id]: server }));
+ Cookies.set("servers", Object.keys(app.servers).join(","));
+ setNewServer("");
+ } catch (e: any) {
+ toast.error(e);
+ }
+ };
+
+ return (
+
+
+
+ {Object.values(app.servers).map(
+ (srv) =>
+ srv && (
+
+
+
+ )
+ )}
+
+
+
+
+
+ );
+}