Merge pull request #29 from kleo-dev/fine-chat-hub

Fine chat hub
This commit is contained in:
Klesti Selimaj
2025-10-26 21:13:14 +01:00
committed by GitHub
5 changed files with 192 additions and 35 deletions
+146 -21
View File
@@ -12,30 +12,56 @@ import { Server } from "@/types/types";
import { motion } from "framer-motion";
import {
ChevronLeft,
MessageCircle,
Users,
PlusCircle,
Settings,
ChevronRight,
Users,
Settings,
TrendingUp,
NetworkIcon,
MessagesSquare,
} from "lucide-react";
import Link from "next/link";
import StartNewDM from "./StartNewDM";
import AsyncValue from "@/components/AsyncValue";
import { useRouter } from "next/navigation";
import Cookies from "js-cookie";
import { toast } from "sonner";
export default function ChatHub() {
const app = useApp();
const router = useRouter();
const recentDMs = app.dms?.slice(0, 5) || [];
// const activeServers = app.servers?.slice(0, 5) || [];
const activeServers: Server[] = [];
const app = useApp();
const recentPeople = app.dms.slice(0, 3);
const activeServers: Server[] = Object.values(app.servers).slice(0, 3);
const trendingServers: Server[] = [
{
id: "cee6b20f-7e6a-4071-8df1-aab37f52bbb9",
name: "ORUS",
icon_url: "https://github.com/orus-dev.png",
channels: [],
},
{
id: "cee6b20f-7e6a-4071-8df1-aab37f52bbb9",
name: "Saturn Client",
icon_url: "https://github.com/saturnclientmc.png",
channels: [],
},
{
id: "cee6b20f-7e6a-4071-8df1-aab37f52bbb9",
name: "OSUI Rust Hub",
icon_url: "https://github.com/osui-rs.png",
channels: [],
},
];
const recentActivity = app.messages.slice(0, 3);
return (
<AppLayout app={app}>
<div className="h-svh w-full max-h-svh flex flex-col py-5 px-5 gap-5">
<div className="h-svh w-full max-h-svh flex flex-col py-6 px-5 md:px-10 gap-6 overflow-y-auto">
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
className="space-y-8"
transition={{ duration: 0.3 }}
className="space-y-10"
>
{/* Greeting */}
<div onClick={() => app.setSidebarOpen(true)}>
@@ -48,22 +74,23 @@ export default function ChatHub() {
</p>
</div>
{/* Recent DMs + Active Servers */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Recent DMs */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MessageCircle className="w-5 h-5" />
Recent DMs
<Users className="w-5 h-5" />
Recently chatted with
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{recentDMs.length ? (
recentDMs.map((dm) => (
{recentPeople.length ? (
recentPeople.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"
className="flex justify-between items-center hover:bg-accent hover:text-accent-foreground rounded p-2 transition"
>
<span className="flex gap-2">
<ProfilePicture
@@ -89,7 +116,7 @@ export default function ChatHub() {
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="w-5 h-5" />
<NetworkIcon className="w-5 h-5" />
Active Servers
</CardTitle>
</CardHeader>
@@ -99,10 +126,13 @@ export default function ChatHub() {
<Link
href={`/server/${srv.id}`}
key={srv.id}
className="flex justify-between items-center hover:bg-accent hover:text-accent-foreground rounded p-1"
className="flex justify-between items-center hover:bg-accent hover:text-accent-foreground rounded p-2 transition"
>
<span className="flex gap-2">
<ProfilePicture name={srv.name} url="" />
<ProfilePicture
name={srv.name}
url={srv.icon_url || ""}
/>
<span className="truncate my-auto">{srv.name}</span>
</span>
<ChevronRight />
@@ -117,11 +147,106 @@ export default function ChatHub() {
</Card>
</div>
{/* Recent Activity Feed */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MessagesSquare className="w-5 h-5" />
Recent Activity
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{recentActivity.length ? (
recentActivity.map((msg) => (
<AsyncValue
key={msg.id + msg.channel_id}
func={() => app.getUserById(msg.from)}
render={({ value: user }) => (
<Link
href={`/chat/${msg.from}`}
className="flex items-center justify-between hover:bg-accent rounded p-2 transition"
>
<div className="flex items-center gap-3">
<ProfilePicture
name={user?.display_name || ""}
url={user?.avatar_url || ""}
/>
<div>
<p className="font-medium">
{user?.display_name || ""}
</p>
<p className="text-sm text-muted-foreground truncate w-[200px]">
{msg.contents}
</p>
</div>
</div>
<ChevronRight className="opacity-70 w-4 h-4" />
</Link>
)}
/>
))
) : (
<p className="text-sm text-muted-foreground">
No recent messages yet.
</p>
)}
</CardContent>
</Card>
{/* Server Discovery */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="w-5 h-5" />
Trending Servers
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{trendingServers.map((srv) => (
<motion.div
key={srv.id}
whileHover={{ scale: 1.03 }}
transition={{ type: "spring", stiffness: 300 }}
>
<Card
className="hover:bg-accent transition"
onClick={async () => {
try {
Cookies.set(
"servers",
Object.keys({
...app.servers,
[srv.id]: srv,
}).join(",")
);
app.setServers((prev) => ({
...prev,
[srv.id]: srv,
}));
router.push(`/server/${srv.id}`);
} catch (e: any) {
toast.error(e);
}
}}
>
<CardContent className="p-4 flex flex-col items-center text-center gap-2">
<ProfilePicture name={srv.name} url={srv.icon_url} />
<h3 className="font-medium truncate">{srv.name}</h3>
</CardContent>
</Card>
</motion.div>
))}
</div>
</CardContent>
</Card>
<Separator />
{/* Quick Actions */}
<div className="w-full h-full flex items-center justify-center">
{/* <h2 className="text-xl font-semibold mb-4">Quick Actions</h2> */}
<div className="w-full flex items-center justify-center">
<div className="flex flex-wrap gap-3">
<StartNewDM />
<Button variant="outline" className="gap-2">
+12
View File
@@ -173,3 +173,15 @@
::-webkit-scrollbar-thumb:hover {
background-color: #4b5563;
}
@layer utilities {
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}
+19
View File
@@ -0,0 +1,19 @@
"use client";
import useAsync from "@/hooks/use-async";
export default function AsyncValue<T, E = any>({
func,
render,
}: Readonly<{
func: () => Promise<T>;
render: ({
loading,
}: {
value: T | undefined;
loading: boolean;
error: E | undefined;
}) => React.ReactNode;
}>) {
return render(useAsync(func));
}
+12 -11
View File
@@ -20,17 +20,18 @@ export default function SidebarDMs({ app }: { app: App }) {
if (!app.profile) return;
const load = async () => {
const users = [
...new Set(
await Promise.all(
app.messages
.sort((a, b) => a.timestamp - b.timestamp)
.flatMap((m) => [m.from, m.channel_id])
.filter((id) => id !== app.profile?.id)
.map((id) => app.getUserById(id))
)
),
].filter((v) => !!v);
const users = (
await Promise.all(
[
...new Set(
app.messages
.sort((a, b) => a.timestamp - b.timestamp)
.flatMap((m) => [m.from, m.channel_id])
.filter((id) => id !== app.profile?.id)
),
].map((id) => app.getUserById(id))
)
).filter((v) => !!v);
app.setDms(users);
+3 -3
View File
@@ -41,7 +41,7 @@ export default function SidebarServers({ app }: { app: App }) {
};
return (
<div className="w-16 flex flex-col items-center gap-4 py-4 border-r h-full">
<div className="w-16 flex flex-col items-center gap-4 py-4 border-r h-full overflow-y-scroll no-scrollbar">
<Button
onClick={() => router.push(`/chat`)}
className="bg-transparent text-accent-foreground hover:text-accent"
@@ -50,9 +50,9 @@ export default function SidebarServers({ app }: { app: App }) {
</Button>
{Object.values(app.servers).map(
(srv) =>
(srv, i) =>
srv && (
<Link href={`/server/${srv.id}`} key={srv.id}>
<Link href={`/server/${srv.id}`} key={i}>
<ProfilePicture name={srv.name} url={srv.icon_url} />
</Link>
)