feat(dm): display active conversations in direct message sidebar
This commit is contained in:
parent
fb5b007861
commit
f312316400
6 changed files with 210 additions and 86 deletions
|
|
@ -1,50 +1,92 @@
|
|||
/**
|
||||
* @file app/(app)/layout.tsx
|
||||
* @description Root application layout wrapping sidebars and main workspace content within the ServerProvider context.
|
||||
* @description Main application layout component that handles user authentication guards, parallel data loading for servers and direct messages, and wraps the app with navigation sidebars and server context.
|
||||
*/
|
||||
|
||||
import { auth } from "@/auth";
|
||||
import { db } from "@/db";
|
||||
import { users } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { users, conversations } from "@/db/schema";
|
||||
import { eq, or } from "drizzle-orm";
|
||||
import { getUserServers } from "@/lib/services/server.service";
|
||||
import { AppSidebar } from "@/components/layout/AppSidebar";
|
||||
import { MemberSidebar } from "@/components/layout/MemberSidebar";
|
||||
import { ServerProvider } from "@/lib/context/ServerContext";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
/**
|
||||
* Renders the primary application layout with authentication checks, database fetching, and sidebar structure.
|
||||
*
|
||||
* @async
|
||||
* @function AppLayout
|
||||
* @param {Object} props - The layout properties.
|
||||
* @param {React.ReactNode} props.children - The nested child route content to be rendered within the layout.
|
||||
* @returns {Promise<JSX.Element>} The rendered application layout container.
|
||||
*/
|
||||
export default async function AppLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const session = await auth();
|
||||
const currentUserId = session?.user?.id;
|
||||
|
||||
if (!session?.user?.id) {
|
||||
// Single Auth Guard
|
||||
if (!currentUserId) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
const [currentUser] = await db
|
||||
.select({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
color: users.color,
|
||||
status: users.status,
|
||||
})
|
||||
.from(users)
|
||||
.where(eq(users.id, session.user.id))
|
||||
.limit(1);
|
||||
// Parallel Loading: User Details, Servers & DM-Conversations
|
||||
const [[currentUser], userServers, userConversations] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
color: users.color,
|
||||
status: users.status,
|
||||
})
|
||||
.from(users)
|
||||
.where(eq(users.id, currentUserId))
|
||||
.limit(1),
|
||||
getUserServers(currentUserId),
|
||||
db.query.conversations.findMany({
|
||||
where: or(
|
||||
eq(conversations.userOneId, currentUserId),
|
||||
eq(conversations.userTwoId, currentUserId),
|
||||
),
|
||||
with: {
|
||||
userOne: true,
|
||||
userTwo: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!currentUser) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
const userServers = await getUserServers(session.user.id);
|
||||
// Transform conversations to isolate the respective conversation partner
|
||||
const formattedConversations = userConversations.map((c) => {
|
||||
const partner = c.userOne.id === currentUserId ? c.userTwo : c.userOne;
|
||||
|
||||
return {
|
||||
id: c.id,
|
||||
partner: {
|
||||
id: partner.id,
|
||||
username: partner.username,
|
||||
color: partner.color,
|
||||
status: partner.status,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<ServerProvider>
|
||||
<div className="flex h-screen w-full overflow-hidden bg-background text-foreground">
|
||||
<AppSidebar servers={userServers} user={currentUser} />
|
||||
<AppSidebar
|
||||
servers={userServers}
|
||||
conversations={formattedConversations}
|
||||
user={currentUser}
|
||||
/>
|
||||
<div className="flex-1 flex min-w-0">{children}</div>
|
||||
<MemberSidebar />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,69 +1,63 @@
|
|||
/**
|
||||
* @file app/(app)/page.tsx
|
||||
* @description Main application layout page featuring the FriendsView.
|
||||
* @description Main application page component that performs user authentication checks, fetches raw friendship data from the database, and renders the friends management view.
|
||||
*/
|
||||
|
||||
import { auth } from "@/auth";
|
||||
import { db } from "@/db";
|
||||
import { friendships } from "@/db/schema";
|
||||
import { or, eq } from "drizzle-orm";
|
||||
import { AppHeader } from "@/components/layout/AppHeader";
|
||||
import { FriendsView, Friendship } from "@/components/friends/FriendsView";
|
||||
import { redirect } from "next/navigation";
|
||||
import { FriendsView, type Friendship } from "@/components/friends/FriendsView";
|
||||
|
||||
/**
|
||||
* Asynchronously renders the main application page for authenticated users.
|
||||
*
|
||||
* @async
|
||||
* @function AppPage
|
||||
* @returns {Promise<JSX.Element>} The rendered application page container with the FriendsView component.
|
||||
*/
|
||||
export default async function AppPage() {
|
||||
const session = await auth();
|
||||
const currentUserId = session?.user?.id;
|
||||
|
||||
let initialFriendships: Friendship[] = [];
|
||||
// Single Auth Guard
|
||||
if (!currentUserId) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
if (session?.user?.id) {
|
||||
const currentUserId = session.user.id;
|
||||
|
||||
const rawFriendships = await db.query.friendships.findMany({
|
||||
where: or(
|
||||
eq(friendships.senderId, currentUserId),
|
||||
eq(friendships.receiverId, currentUserId),
|
||||
),
|
||||
with: {
|
||||
sender: {
|
||||
columns: {
|
||||
id: true,
|
||||
username: true,
|
||||
color: true,
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
receiver: {
|
||||
columns: {
|
||||
id: true,
|
||||
username: true,
|
||||
color: true,
|
||||
status: true,
|
||||
},
|
||||
// Retrieve all of the user's friendships
|
||||
const rawFriendships = await db.query.friendships.findMany({
|
||||
where: or(
|
||||
eq(friendships.senderId, currentUserId),
|
||||
eq(friendships.receiverId, currentUserId),
|
||||
),
|
||||
with: {
|
||||
sender: {
|
||||
columns: {
|
||||
id: true,
|
||||
username: true,
|
||||
color: true,
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
initialFriendships = rawFriendships as Friendship[];
|
||||
}
|
||||
receiver: {
|
||||
columns: {
|
||||
id: true,
|
||||
username: true,
|
||||
color: true,
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full bg-background p-4">
|
||||
{session?.user?.id ? (
|
||||
<FriendsView
|
||||
currentUserId={session.user.id}
|
||||
initialFriendships={initialFriendships}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<AppHeader />
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<h2 className="text-xl font-bold text-white mb-2">Welcome back!</h2>
|
||||
<p className="text-muted max-w-sm">
|
||||
Please sign in to view your friends list or start a conversation.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<FriendsView
|
||||
currentUserId={currentUserId}
|
||||
initialFriendships={rawFriendships as Friendship[]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,16 @@ import { useSidebarStore } from "@/lib/stores/useSidebarStore";
|
|||
import { ServerSidebar } from "@/components/sidebar/ServerSidebar";
|
||||
import { ChannelSidebar } from "@/components/sidebar/ChannelSidebar";
|
||||
import { UserPanel } from "@/components/sidebar/UserPanel";
|
||||
import type { ServerWithChannels } from "@/lib/context/ServerContext";
|
||||
import {
|
||||
useActiveServer,
|
||||
type ServerWithChannels,
|
||||
} from "@/lib/context/ServerContext";
|
||||
import type { UserStatus } from "@/db/schema";
|
||||
import { clsx } from "clsx";
|
||||
import {
|
||||
DirectMessageSidebar,
|
||||
SidebarConversation,
|
||||
} from "../sidebar/DirectMessageSidebar";
|
||||
|
||||
/**
|
||||
* Properties representing the user in the sidebar.
|
||||
|
|
@ -32,10 +39,12 @@ export interface SidebarUser {
|
|||
*
|
||||
* @interface AppSidebarProps
|
||||
* @property {ServerWithChannels[]} servers - List of available servers including their channels.
|
||||
* @property {SidebarConversation[]} conversations - List of direct message conversations.
|
||||
* @property {SidebarUser} user - Information about the currently authenticated user.
|
||||
*/
|
||||
interface AppSidebarProps {
|
||||
servers: ServerWithChannels[];
|
||||
conversations: SidebarConversation[];
|
||||
user: SidebarUser;
|
||||
}
|
||||
|
||||
|
|
@ -43,10 +52,18 @@ interface AppSidebarProps {
|
|||
* Renders the responsive application sidebar containing server navigation, channel lists, and user profile.
|
||||
*
|
||||
* @param {AppSidebarProps} props - The component props.
|
||||
* @param {ServerWithChannels[]} props.servers - List of available servers including their channels.
|
||||
* @param {SidebarConversation[]} props.conversations - List of direct message conversations.
|
||||
* @param {SidebarUser} props.user - Information about the currently authenticated user.
|
||||
* @returns {JSX.Element} The rendered mobile overlay and responsive sidebar structure.
|
||||
*/
|
||||
export function AppSidebar({ servers, user }: AppSidebarProps) {
|
||||
export function AppSidebar({
|
||||
servers,
|
||||
user,
|
||||
conversations = [],
|
||||
}: AppSidebarProps) {
|
||||
const { isNavOpen, closeAll } = useSidebarStore();
|
||||
const { activeServer } = useActiveServer();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -59,7 +76,11 @@ export function AppSidebar({ servers, user }: AppSidebarProps) {
|
|||
>
|
||||
<div className="flex flex-1 min-h-0 w-78">
|
||||
<ServerSidebar servers={servers} />
|
||||
<ChannelSidebar />
|
||||
{!activeServer ? (
|
||||
<DirectMessageSidebar conversations={conversations} />
|
||||
) : (
|
||||
<ChannelSidebar />
|
||||
)}
|
||||
</div>
|
||||
<div className="w-78">
|
||||
<UserPanel user={user} />
|
||||
|
|
@ -82,7 +103,11 @@ export function AppSidebar({ servers, user }: AppSidebarProps) {
|
|||
>
|
||||
<div className="flex flex-1 min-h-0 w-full">
|
||||
<ServerSidebar servers={servers} />
|
||||
<ChannelSidebar />
|
||||
{!activeServer ? (
|
||||
<DirectMessageSidebar conversations={conversations} />
|
||||
) : (
|
||||
<ChannelSidebar />
|
||||
)}
|
||||
</div>
|
||||
<UserPanel user={user} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { useState } from "react";
|
|||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useActiveServer } from "@/lib/context/ServerContext";
|
||||
import { DirectMessageSidebar } from "@/components/sidebar/DirectMessageSidebar";
|
||||
import { useSidebarStore } from "@/lib/stores/useSidebarStore";
|
||||
import { CreateChannelModal } from "@/components/modals/CreateChannelModal";
|
||||
import { EditChannelModal } from "@/components/modals/EditChannelModal";
|
||||
|
|
@ -19,7 +18,7 @@ import type { Channel } from "@/db/schema";
|
|||
/**
|
||||
* Renders the channel sidebar for the active server with text channel lists, creation triggers, and settings handlers.
|
||||
*
|
||||
* @returns {JSX.Element} The rendered channel sidebar container.
|
||||
* @returns {JSX.Element | null} The rendered channel sidebar container or null if no active server exists.
|
||||
*/
|
||||
export function ChannelSidebar() {
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
|
|
@ -30,6 +29,10 @@ export function ChannelSidebar() {
|
|||
const { activeServer } = useActiveServer();
|
||||
const { closeNav, toggleNav } = useSidebarStore();
|
||||
|
||||
if (!activeServer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles channel selection clicks, automatically closing the mobile navigation drawer on smaller screens.
|
||||
*
|
||||
|
|
@ -56,10 +59,6 @@ export function ChannelSidebar() {
|
|||
setEditingChannel(channel);
|
||||
};
|
||||
|
||||
if (!activeServer) {
|
||||
return <DirectMessageSidebar />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex-1 w-full bg-surface/50 border-r border-background flex flex-col h-full min-w-0 overflow-hidden">
|
||||
|
|
|
|||
|
|
@ -6,17 +6,47 @@
|
|||
"use client";
|
||||
|
||||
import { useSidebarStore } from "@/lib/stores/useSidebarStore";
|
||||
import { PanelLeftClose } from "lucide-react";
|
||||
import { PanelLeftClose, Users } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { clsx } from "clsx";
|
||||
import { UserAvatar } from "@/components/ui/UserAvatar";
|
||||
import { FriendUser } from "../friends/FriendsView";
|
||||
|
||||
/**
|
||||
* DirectMessageSidebar component that renders navigation for direct messages and friends.
|
||||
* Includes a header with collapsible controls and links for viewing chat channels and active conversations.
|
||||
* Represents a direct message conversation in the sidebar.
|
||||
*
|
||||
* @returns {JSX.Element} The direct message sidebar component layout.
|
||||
* @interface SidebarConversation
|
||||
* @property {string} id - The unique identifier of the conversation.
|
||||
* @property {FriendUser} partner - The other user participating in the conversation.
|
||||
*/
|
||||
export function DirectMessageSidebar() {
|
||||
export interface SidebarConversation {
|
||||
id: string;
|
||||
partner: FriendUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties for the DirectMessageSidebar component.
|
||||
*
|
||||
* @interface DirectMessageSidebarProps
|
||||
* @property {SidebarConversation[]} [conversations=[]] - Array of active direct message conversations.
|
||||
*/
|
||||
interface DirectMessageSidebarProps {
|
||||
conversations?: SidebarConversation[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the direct message sidebar with navigation options, friends view link, and a list of active DM chats.
|
||||
*
|
||||
* @param {DirectMessageSidebarProps} props - The component props.
|
||||
* @param {SidebarConversation[]} [props.conversations=[]] - Array of active direct message conversations.
|
||||
* @returns {JSX.Element} The rendered direct message sidebar.
|
||||
*/
|
||||
export function DirectMessageSidebar({
|
||||
conversations = [],
|
||||
}: DirectMessageSidebarProps) {
|
||||
const { toggleNav } = useSidebarStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div className="flex-1 w-full md:w-60 bg-surface/50 border-r border-background flex flex-col h-full shrink-0">
|
||||
|
|
@ -38,19 +68,52 @@ export function DirectMessageSidebar() {
|
|||
<div className="space-y-1">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-3 px-3 py-2 rounded-md text-sm text-white bg-accent/20 font-medium transition-all"
|
||||
className={clsx(
|
||||
"flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-all",
|
||||
pathname === "/"
|
||||
? "text-white bg-accent/20"
|
||||
: "text-muted hover:text-white hover:bg-surface/50",
|
||||
)}
|
||||
>
|
||||
<span>Friends</span>
|
||||
<Users className="w-4 h-4" />
|
||||
<span className="font-bold">Friends</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-muted px-2 mb-2 uppercase tracking-wider">
|
||||
Direct Messages
|
||||
</div>
|
||||
<div className="text-sm text-muted px-2 py-1 italic">
|
||||
No active chats
|
||||
All Messages
|
||||
</div>
|
||||
|
||||
{conversations.length === 0 ? (
|
||||
<div className="text-sm text-muted px-2 py-1 italic">
|
||||
No active chats
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-0.5">
|
||||
{conversations.map((c) => {
|
||||
const isActive = pathname === `/dm/${c.id}`;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={c.id}
|
||||
href={`/dm/${c.id}`}
|
||||
className={clsx(
|
||||
"flex items-center gap-3 px-2 py-1.5 rounded-md text-sm font-medium transition-colors",
|
||||
isActive
|
||||
? "bg-accent/20 text-white"
|
||||
: "text-muted hover:text-white hover:bg-surface/50",
|
||||
)}
|
||||
>
|
||||
{/* UserAvatar Integration */}
|
||||
<UserAvatar user={c.partner} size="xs" />
|
||||
|
||||
<span className="truncate">{c.partner.username}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { clsx } from "clsx";
|
|||
*
|
||||
* @type AvatarSize
|
||||
*/
|
||||
type AvatarSize = "sm" | "md";
|
||||
type AvatarSize = "xs" | "sm" | "md";
|
||||
|
||||
/**
|
||||
* User object properties required to render the avatar.
|
||||
|
|
@ -46,6 +46,7 @@ interface UserAvatarProps {
|
|||
}
|
||||
|
||||
const SIZE_MAP: Record<AvatarSize, { container: string; badge: string }> = {
|
||||
xs: { container: "w-6 h-6 text-xs", badge: "w-1.5 h-1.5" },
|
||||
sm: { container: "w-8 h-8 text-xs", badge: "w-2.5 h-2.5" },
|
||||
md: { container: "w-10 h-10 text-sm", badge: "w-3 h-3" },
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue