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 && ( - - - - ) - )} - - - - - - - - - Join a server - - -
-
- - setNewServer(e.target.value)} - placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - /> -
-
- - - - - - - - - -
-
- -
- -
-
- - {/* 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 && ( + + + + ) + )} + + + + + + + + Join a server + + +
+ + setNewServer(e.target.value)} + placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + /> +
+ + + + + + + + + +
+
+ + +
+ ); +}