refactor(members): introduce UserProvider to eliminate prop drilling for member profile popovers
All checks were successful
Deploy Waveform to VPS / deploy (push) Successful in 1m4s
All checks were successful
Deploy Waveform to VPS / deploy (push) Successful in 1m4s
This commit is contained in:
parent
e2985c53a2
commit
3a119c5891
9 changed files with 87 additions and 120 deletions
|
|
@ -13,7 +13,6 @@ import { ChatInput } from "@/components/chat/ChatInput";
|
|||
import { ChatMessages } from "@/components/chat/ChatMessages";
|
||||
import type { MessageWithMember } from "@/components/chat/ChatItem";
|
||||
import { isValidUuid } from "@/lib/utils";
|
||||
import { getUserFriendships } from "@/lib/services/friends.service";
|
||||
import AppFooter from "@/components/layout/AppFooter";
|
||||
|
||||
/** Renders the direct message conversation page with header, message history, and input field. */
|
||||
|
|
@ -35,8 +34,8 @@ export default async function DirectMessagePage({
|
|||
redirect("/login");
|
||||
}
|
||||
|
||||
// 3. Database Queries
|
||||
const [conversation, rawMessages, friendships] = await Promise.all([
|
||||
// 3. Database Queries (Freundschaften werden nun global im Layout geladen)
|
||||
const [conversation, rawMessages] = await Promise.all([
|
||||
db.query.conversations.findFirst({
|
||||
where: and(
|
||||
eq(conversations.id, conversationId),
|
||||
|
|
@ -57,7 +56,6 @@ export default async function DirectMessagePage({
|
|||
},
|
||||
orderBy: (dm, { asc }) => [asc(dm.createdAt)],
|
||||
}),
|
||||
getUserFriendships(session.user.id),
|
||||
]);
|
||||
|
||||
if (!conversation) {
|
||||
|
|
@ -90,8 +88,6 @@ export default async function DirectMessagePage({
|
|||
type="dm"
|
||||
name={partner.username}
|
||||
initialMessages={initialMessages}
|
||||
currentUserId={session.user.id}
|
||||
userFriendships={friendships}
|
||||
/>
|
||||
|
||||
<ChatInput
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { MemberSidebar } from "@/components/layout/MemberSidebar";
|
|||
import { ServerProvider } from "@/lib/context/ServerContext";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getUserFriendships } from "@/lib/services/friends.service";
|
||||
import { UserProvider } from "@/lib/context/UserContext";
|
||||
|
||||
/** Renders the primary application layout with authentication checks, database fetching, and sidebar structure. */
|
||||
export default async function AppLayout({
|
||||
|
|
@ -30,16 +31,7 @@ export default async function AppLayout({
|
|||
|
||||
// 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),
|
||||
db.select().from(users).where(eq(users.id, currentUserId)).limit(1),
|
||||
getUserServers(currentUserId),
|
||||
db.query.conversations.findMany({
|
||||
where: or(
|
||||
|
|
@ -78,6 +70,7 @@ export default async function AppLayout({
|
|||
]);
|
||||
|
||||
return (
|
||||
<UserProvider currentUser={currentUser} friendships={friendships}>
|
||||
<ServerProvider>
|
||||
<div className="flex h-screen w-full overflow-hidden bg-background text-foreground">
|
||||
<AppSidebar
|
||||
|
|
@ -86,11 +79,9 @@ export default async function AppLayout({
|
|||
user={currentUser}
|
||||
/>
|
||||
<div className="flex-1 flex min-w-0">{children}</div>
|
||||
<MemberSidebar
|
||||
currentUserId={currentUserId}
|
||||
userFriendships={friendships}
|
||||
/>
|
||||
<MemberSidebar />
|
||||
</div>
|
||||
</ServerProvider>
|
||||
</UserProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import { getChannelById } from "@/lib/services/channel.service";
|
|||
import { getChannelMessages } from "@/lib/services/message.service";
|
||||
import { getServerById } from "@/lib/services/server.service";
|
||||
import { isValidUuid } from "@/lib/utils";
|
||||
import { getUserFriendships } from "@/lib/services/friends.service";
|
||||
import AppFooter from "@/components/layout/AppFooter";
|
||||
|
||||
/** Renders the channel chat view by validating parameters, checking user session, fetching channel data, friendships, and displaying headers, messages, and input controls. */
|
||||
|
|
@ -34,11 +33,10 @@ export default async function ChannelPage({
|
|||
}
|
||||
|
||||
// 3. Parallel Loading of Data
|
||||
const [server, channel, channelMessages, friendships] = await Promise.all([
|
||||
const [server, channel, channelMessages] = await Promise.all([
|
||||
getServerById(serverId),
|
||||
getChannelById(channelId),
|
||||
getChannelMessages(channelId),
|
||||
getUserFriendships(session.user.id),
|
||||
]);
|
||||
|
||||
if (!channel || !server) {
|
||||
|
|
@ -57,8 +55,6 @@ export default async function ChannelPage({
|
|||
type="chat"
|
||||
name={channel.name}
|
||||
initialMessages={channelMessages}
|
||||
currentUserId={session.user.id}
|
||||
userFriendships={friendships}
|
||||
/>
|
||||
|
||||
<ChatInput
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { ChatItemActions } from "./ChatItemActions";
|
|||
import { ChatItemEdit } from "./ChatItemEdit";
|
||||
import { UserProfilePopover } from "../ui/UserProfilePopover";
|
||||
import { useActiveServer } from "@/lib/context/ServerContext";
|
||||
import { useUser } from "@/lib/context/UserContext";
|
||||
|
||||
/** Composite message type extending base database Message with channel/conversation details and member relation. */
|
||||
export type MessageWithMember = Omit<Message, "channelId"> & {
|
||||
|
|
@ -30,12 +31,6 @@ export type MessageWithMember = Omit<Message, "channelId"> & {
|
|||
interface ChatItemProps {
|
||||
type: "chat" | "dm";
|
||||
message: MessageWithMember;
|
||||
userFriendships: Array<{
|
||||
senderId: string;
|
||||
receiverId: string;
|
||||
status: string;
|
||||
}>;
|
||||
currentUserId: string;
|
||||
onDeleteSuccess?: (id: string) => void;
|
||||
onEditSuccess?: (id: string, newContent: string) => void;
|
||||
}
|
||||
|
|
@ -44,13 +39,14 @@ interface ChatItemProps {
|
|||
export function ChatItem({
|
||||
type,
|
||||
message,
|
||||
currentUserId,
|
||||
userFriendships,
|
||||
onDeleteSuccess,
|
||||
onEditSuccess,
|
||||
}: ChatItemProps) {
|
||||
const router = useRouter();
|
||||
const { setActiveServer } = useActiveServer();
|
||||
const { currentUser, friendships } = useUser();
|
||||
|
||||
const currentUserId = currentUser.id;
|
||||
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
|
@ -113,15 +109,11 @@ export function ChatItem({
|
|||
|
||||
/** Determines the friendship status between the current user and the message author. */
|
||||
const getFriendshipStatus = () => {
|
||||
if (
|
||||
!user?.id ||
|
||||
user.id === currentUserId ||
|
||||
!Array.isArray(userFriendships)
|
||||
) {
|
||||
if (!user?.id || user.id === currentUserId || !Array.isArray(friendships)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const friendship = userFriendships.find(
|
||||
const friendship = friendships.find(
|
||||
(f) =>
|
||||
(f.senderId === user.id && f.receiverId === currentUserId) ||
|
||||
(f.receiverId === user.id && f.senderId === currentUserId),
|
||||
|
|
|
|||
|
|
@ -13,12 +13,6 @@ export interface ChatMessagesProps {
|
|||
type: "chat" | "dm";
|
||||
name: string;
|
||||
initialMessages: MessageWithMember[];
|
||||
userFriendships: Array<{
|
||||
senderId: string;
|
||||
receiverId: string;
|
||||
status: string;
|
||||
}>;
|
||||
currentUserId: string;
|
||||
onDeleteMessage?: (id: string) => void;
|
||||
onEditMessage?: (id: string, newContent: string) => void;
|
||||
}
|
||||
|
|
@ -55,8 +49,6 @@ export function ChatMessages({
|
|||
type,
|
||||
name,
|
||||
initialMessages,
|
||||
currentUserId,
|
||||
userFriendships,
|
||||
onDeleteMessage,
|
||||
onEditMessage,
|
||||
}: ChatMessagesProps) {
|
||||
|
|
@ -156,8 +148,6 @@ export function ChatMessages({
|
|||
<ChatItem
|
||||
type={type}
|
||||
message={message}
|
||||
currentUserId={currentUserId}
|
||||
userFriendships={userFriendships}
|
||||
onDeleteSuccess={handleDeleteMessage}
|
||||
onEditSuccess={handleEditMessage}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -19,16 +19,6 @@ interface MemberHeaderProps {
|
|||
onClose: () => void;
|
||||
}
|
||||
|
||||
/** Properties for the MemberSidebar component. */
|
||||
interface MemberSidebarProps {
|
||||
currentUserId: string;
|
||||
userFriendships: Array<{
|
||||
senderId: string;
|
||||
receiverId: string;
|
||||
status: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** Renders the header section of the member sidebar with a title and close button. */
|
||||
function MemberHeader({ title, onClose }: MemberHeaderProps) {
|
||||
return (
|
||||
|
|
@ -49,10 +39,7 @@ function MemberHeader({ title, onClose }: MemberHeaderProps) {
|
|||
}
|
||||
|
||||
/** Displays the list of members for the active server in a responsive sidebar or drawer layout. */
|
||||
export function MemberSidebar({
|
||||
currentUserId,
|
||||
userFriendships = [],
|
||||
}: MemberSidebarProps) {
|
||||
export function MemberSidebar() {
|
||||
const { isMembersOpen, closeMembers } = useSidebarStore();
|
||||
const { activeServer } = useActiveServer();
|
||||
const desktopSidebarRef = useRef<HTMLElement>(null);
|
||||
|
|
@ -124,13 +111,7 @@ export function MemberSidebar({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<MemberList
|
||||
members={members}
|
||||
currentUserId={currentUserId}
|
||||
userFriendships={userFriendships}
|
||||
/>
|
||||
);
|
||||
return <MemberList members={members} />;
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -13,29 +13,22 @@ import { UserAvatar } from "../ui/UserAvatar";
|
|||
import { UserProfilePopover } from "../ui/UserProfilePopover";
|
||||
import { useActiveServer } from "@/lib/context/ServerContext";
|
||||
import { useSidebarStore } from "@/lib/stores/useSidebarStore";
|
||||
import { useUser } from "@/lib/context/UserContext";
|
||||
|
||||
/** Props for the MemberItem component. */
|
||||
interface MemberItemProps {
|
||||
member: User;
|
||||
currentUserId: string;
|
||||
userFriendships: Array<{
|
||||
senderId: string;
|
||||
receiverId: string;
|
||||
status: string;
|
||||
}>;
|
||||
isOffline?: boolean;
|
||||
}
|
||||
|
||||
/** Renders an individual member item with an interactive user profile popover. */
|
||||
export function MemberItem({
|
||||
member,
|
||||
currentUserId,
|
||||
userFriendships = [],
|
||||
isOffline = false,
|
||||
}: MemberItemProps) {
|
||||
export function MemberItem({ member, isOffline = false }: MemberItemProps) {
|
||||
const router = useRouter();
|
||||
const { setActiveServer } = useActiveServer();
|
||||
const { closeMembers } = useSidebarStore();
|
||||
const { currentUser, friendships } = useUser();
|
||||
|
||||
const currentUserId = currentUser.id;
|
||||
|
||||
const [isProfileOpen, setIsProfileOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -71,11 +64,11 @@ export function MemberItem({
|
|||
|
||||
/** Determines the friendship status between the logged-in user and this member. */
|
||||
const getFriendshipStatus = () => {
|
||||
if (!member.id || member.id === currentUserId || !userFriendships) {
|
||||
if (!member.id || member.id === currentUserId || !friendships) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const friendship = userFriendships.find(
|
||||
const friendship = friendships.find(
|
||||
(f) =>
|
||||
(f.senderId === member.id && f.receiverId === currentUserId) ||
|
||||
(f.receiverId === member.id && f.senderId === currentUserId),
|
||||
|
|
|
|||
|
|
@ -1,29 +1,18 @@
|
|||
/**
|
||||
* @file components/members/MemberList.tsx
|
||||
* @description Renders categorized lists of online and offline members with their friendship statuses.
|
||||
* @description Renders categorized lists of online and offline members using the UserContext.
|
||||
*/
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { MemberItem } from "@/components/members/MemberItem";
|
||||
import { User } from "@/db/schema";
|
||||
|
||||
/** Props for the MemberList component. */
|
||||
interface MemberListProps {
|
||||
members: User[];
|
||||
currentUserId: string;
|
||||
userFriendships: Array<{
|
||||
senderId: string;
|
||||
receiverId: string;
|
||||
status: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** Renders online and offline community members in distinct sections. */
|
||||
export function MemberList({
|
||||
members = [],
|
||||
currentUserId,
|
||||
userFriendships = [],
|
||||
}: MemberListProps) {
|
||||
export function MemberList({ members = [] }: MemberListProps) {
|
||||
const { onlineMembers, offlineMembers } = useMemo(() => {
|
||||
return {
|
||||
onlineMembers: members.filter((m) => m.status !== "OFFLINE"),
|
||||
|
|
@ -40,12 +29,7 @@ export function MemberList({
|
|||
</h2>
|
||||
<div className="space-y-0.5">
|
||||
{onlineMembers.map((member) => (
|
||||
<MemberItem
|
||||
key={member.id}
|
||||
member={member}
|
||||
currentUserId={currentUserId}
|
||||
userFriendships={userFriendships}
|
||||
/>
|
||||
<MemberItem key={member.id} member={member} />
|
||||
))}
|
||||
{onlineMembers.length === 0 && (
|
||||
<p className="text-xs text-muted/60 px-2 italic">
|
||||
|
|
@ -62,13 +46,7 @@ export function MemberList({
|
|||
</h2>
|
||||
<div className="space-y-0.5">
|
||||
{offlineMembers.map((member) => (
|
||||
<MemberItem
|
||||
key={member.id}
|
||||
member={member}
|
||||
currentUserId={currentUserId}
|
||||
userFriendships={userFriendships}
|
||||
isOffline
|
||||
/>
|
||||
<MemberItem key={member.id} member={member} isOffline />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
50
lib/context/UserContext.tsx
Normal file
50
lib/context/UserContext.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* @file lib/context/UserContext.tsx
|
||||
* @description Context provider and hook for managing current user data and friendship states.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext } from "react";
|
||||
import type { User } from "@/db/schema";
|
||||
|
||||
/** Represents a friendship record between two users. */
|
||||
interface Friendship {
|
||||
senderId: string;
|
||||
receiverId: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** Shape of the user context value. */
|
||||
interface UserContextType {
|
||||
currentUser: User;
|
||||
friendships: Friendship[];
|
||||
}
|
||||
|
||||
const UserContext = createContext<UserContextType | null>(null);
|
||||
|
||||
/** Provides user data and friendship states to child components. */
|
||||
export function UserProvider({
|
||||
children,
|
||||
currentUser,
|
||||
friendships,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
currentUser: User;
|
||||
friendships: Friendship[];
|
||||
}) {
|
||||
return (
|
||||
<UserContext.Provider value={{ currentUser, friendships }}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/** Custom hook to consume user context data. */
|
||||
export function useUser() {
|
||||
const context = useContext(UserContext);
|
||||
if (!context) {
|
||||
throw new Error("useUser must be used within a UserProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Loading…
Reference in a new issue