Merge pull request #23 from kleo-dev/chat-hub

Chat hub
This commit is contained in:
Klesti Selimaj
2025-10-17 22:53:50 +02:00
committed by GitHub
6 changed files with 162 additions and 31 deletions
+128 -7
View File
@@ -1,24 +1,145 @@
"use client";
import ProfilePicture from "@/components/ProfilePicture";
import SettingsDialog from "@/components/settings/SettingsDialog";
import AppSidebar from "@/components/Sidebar";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useIsMobile } from "@/hooks/is-mobile";
import { Separator } from "@/components/ui/separator";
import useApp from "@/hooks/use-app";
import { ProfileSettings } from "@/types/settings";
import { Server } from "@/types/types";
import { motion } from "framer-motion";
import {
ChevronLeft,
MessageCircle,
Users,
PlusCircle,
Settings,
ChevronRight,
} from "lucide-react";
import Link from "next/link";
export default function Chat() {
export default function ChatHub() {
const app = useApp();
const isMobile = useIsMobile();
const recentDMs = app.dms?.slice(0, 5) || [];
// const activeServers = app.servers?.slice(0, 5) || [];
const activeServers: Server[] = [];
return (
<AppSidebar app={app}>
<Card className="bg-background w-full">
<div className="h-svh w-full max-h-svh flex flex-col py-5 px-5 gap-5">
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
className="space-y-8"
>
{/* Greeting */}
<div onClick={() => app.setSidebarOpen(true)}>
<h1 className="text-3xl font-bold flex cursor-pointer gap-2">
<ChevronLeft className="my-auto w-5" />
Hey {app.profile?.display_name || "there"}
</h1>
<p className="text-muted-foreground">
Here's what's happening in your network today.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Recent DMs */}
<Card>
<CardHeader>
<CardTitle onClick={() => isMobile && app.setSidebarOpen(true)}>
Direct Messages
<CardTitle className="flex items-center gap-2">
<MessageCircle className="w-5 h-5" />
Recent DMs
</CardTitle>
</CardHeader>
<CardContent></CardContent>
<CardContent className="space-y-3">
{recentDMs.length ? (
recentDMs.map((dm) => (
<Link
href={`/chat/${dm.id}`}
key={dm.id}
className="flex justify-between items-center hover:bg-accent hover:text-accent-foreground rounded p-1"
>
<span className="flex gap-2">
<ProfilePicture
name={dm.display_name}
url={dm.avatar_url}
/>
<span className="truncate my-auto">
{dm.display_name || dm.id}
</span>
</span>
<ChevronRight />
</Link>
))
) : (
<p className="text-sm text-muted-foreground">
No recent DMs yet.
</p>
)}
</CardContent>
</Card>
{/* Active Servers */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="w-5 h-5" />
Active Servers
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{activeServers.length ? (
activeServers.map((srv) => (
<Link
href={`/server/${srv.id}`}
key={srv.id}
className="flex justify-between items-center hover:bg-accent hover:text-accent-foreground rounded p-1"
>
<span className="flex gap-2">
<ProfilePicture name={srv.name} url="" />
<span className="truncate my-auto">{srv.name}</span>
</span>
<ChevronRight />
</Link>
))
) : (
<p className="text-sm text-muted-foreground">
No servers joined yet.
</p>
)}
</CardContent>
</Card>
</div>
<Separator />
{/* Quick Actions */}
<div>
<h2 className="text-xl font-semibold mb-4">Quick Actions</h2>
<div className="flex flex-wrap gap-3">
<Button variant="outline" className="gap-2">
<PlusCircle className="w-4 h-4" />
Start New DM
</Button>
<Button variant="outline" className="gap-2">
<Users className="w-4 h-4" />
Create Server
</Button>
<SettingsDialog profile={app.profile as ProfileSettings}>
<Button variant="outline" className="gap-2">
<Settings className="w-4 h-4" />
Settings
</Button>
</SettingsDialog>
</div>
</div>
</motion.div>
</div>
</AppSidebar>
);
}
+2 -5
View File
@@ -11,15 +11,13 @@ import {
DropdownMenuTrigger,
} from "./ui/dropdown-menu";
import { ChevronLeftIcon, SendIcon, SmilePlusIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { useRef, useState } from "react";
import { format, isToday, isYesterday } from "date-fns";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { UserProfile } from "@/hooks/get-user";
import axios from "axios";
import ProfilePicture from "./ProfilePicture";
import App from "@/types/app";
import { useIsMobile } from "@/hooks/use-mobile";
import { Textarea } from "./ui/textarea";
function MessageContainer({ message, app }: { message: Message; app: App }) {
@@ -130,7 +128,6 @@ export default function MessageBox({
messages: Message[];
}) {
const [text, setText] = useState("");
const isMobile = useIsMobile();
const inputRef = useRef<any>(null);
return (
@@ -138,7 +135,7 @@ export default function MessageBox({
{channelName && (
<header className="h-12 py-4 flex items-center border-b text-sm font-semibold">
<span
onClick={() => isMobile && app.setSidebarOpen(true)}
onClick={() => app.setSidebarOpen(true)}
className="cursor-pointer flex"
>
<ChevronLeftIcon className="w-5 h-5" />
+7 -6
View File
@@ -36,6 +36,7 @@ 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";
export default function AppSidebar({
children,
@@ -54,7 +55,7 @@ export default function AppSidebar({
const [newServer, setNewServer] = useState({ name: "", ip: "" });
const [query, setQuery] = useState("");
const router = useRouter();
const [filteredDms, setFilteredDms] = useState<UserProfile[]>([]);
const isMobile = useIsMobile();
const dms = useMemo(() => {
return Array.from(
@@ -69,7 +70,7 @@ export default function AppSidebar({
useEffect(() => {
const load = async () => {
const users = await Promise.all(dms.map((id) => app.getUserById(id)));
setFilteredDms(
app.setDms(
users.filter(
(user) =>
user !== null &&
@@ -139,7 +140,7 @@ export default function AppSidebar({
<div
className={cn(
"border-r transition-all duration-300 ease-in-out overflow-hidden flex",
app.sidebarOpen ? "w-5xl md:w-md" : "w-0 md:w-md"
app.sidebarOpen && isMobile ? "w-5xl md:w-md" : "w-0 md:w-md"
)}
>
<div className="w-16 flex flex-col items-center gap-4 py-4 bg-muted border-r">
@@ -258,7 +259,7 @@ export default function AppSidebar({
</>
) : (
<>
{filteredDms.map((user) => (
{app.dms.map((user) => (
<DMItem
name={user.display_name}
id={user.id}
@@ -268,7 +269,7 @@ export default function AppSidebar({
app={app}
/>
))}
{filteredDms.length === 0 && query.trim() !== "" ? (
{app.dms.length === 0 && query.trim() !== "" ? (
<div className="px-3 py-2">
<Button
variant="ghost"
@@ -303,7 +304,7 @@ export default function AppSidebar({
</footer>
</div>
</div>
{app.sidebarOpen ? (
{app.sidebarOpen && isMobile ? (
<div
className="w-full h-full overflow-x-hidden"
onClick={() => app.setSidebarOpen(false)}
+7 -1
View File
@@ -10,20 +10,25 @@ import {
import { cn } from "@/lib/utils";
import Settings from "./Settings";
import { ProfileSettings } from "@/types/settings";
import { useState } from "react";
import { ReactNode, useState } from "react";
export default function SettingsDialog({
className,
tab,
profile,
children,
}: {
className?: string;
tab?: string;
profile: ProfileSettings;
children?: ReactNode;
}) {
return (
<Dialog>
<DialogTrigger asChild>
{children ? (
children
) : (
<Button
className={cn(
"bg-transparent text-accent-foreground hover:text-accent",
@@ -32,6 +37,7 @@ export default function SettingsDialog({
>
<SettingsIcon />
</Button>
)}
</DialogTrigger>
<DialogContent>
<DialogHeader>
+3
View File
@@ -13,8 +13,11 @@ export default function useApp(): App {
const [sidebarOpen, setSidebarOpen] = useState(false);
const profile = useUser();
const [clientSettings, setClientSettings] = useClientSettings();
const [dms, setDms] = useState<UserProfile[]>([]);
return {
dms,
setDms,
node,
clientSettings,
setClientSettings,
+3
View File
@@ -23,4 +23,7 @@ export default interface App {
setSidebarOpen: SetState<boolean>;
getUserById: (userId: string) => Promise<UserProfile | null>;
dms: UserProfile[];
setDms: SetState<UserProfile[]>;
}