added chat messages component and query provider

This commit is contained in:
Bob Burningham 2023-10-27 22:45:31 -07:00
parent 55ad18e04a
commit 70141f46fd
7 changed files with 301 additions and 2 deletions

View File

@ -4,6 +4,7 @@ import { db } from "@/lib/db";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { ChatHeader } from "@/components/chat/chat-header"; import { ChatHeader } from "@/components/chat/chat-header";
import { ChatInput } from "@/components/chat/chat-input"; import { ChatInput } from "@/components/chat/chat-input";
import { ChatMessages } from "@/components/chat/chat-messages";
interface ChannelIdPageProps { interface ChannelIdPageProps {
params: { params: {
@ -42,7 +43,20 @@ const ChannelIdPage = async ({
return ( return (
<div className="bg-white dark:bg-[#313338] flex flex-col h-full"> <div className="bg-white dark:bg-[#313338] flex flex-col h-full">
<ChatHeader name={channel.name} serverId={channel.serverId} type={"channel"}/> <ChatHeader name={channel.name} serverId={channel.serverId} type={"channel"}/>
<div className="flex-1">Future Messages</div> <ChatMessages
member={member}
name={channel.name}
chatId={channel.id}
type="channel"
apiUrl="/api/messages"
socketUrl="/api/socket/messages"
socketQuery={{
channelId: channel.id,
serverId: channel.serverId,
}}
paramKey="channelId"
paramValue={channel.id}
/>
<ChatInput name={channel.name} type="channel" apiUrl="/api/socket/messages" query={{ <ChatInput name={channel.name} type="channel" apiUrl="/api/socket/messages" query={{
channelId: channel.id, channelId: channel.id,
serverId: channel.serverId, serverId: channel.serverId,

83
app/api/messages/route.ts Normal file
View File

@ -0,0 +1,83 @@
import { currentProfile } from "@/lib/current-profile";
import { db } from "@/lib/db";
import { Message } from "@prisma/client";
import { NextResponse } from "next/server";
const MESSAGES_BATCH = 10;
export async function GET(
req: Request
) {
try {
const profile = await currentProfile();
const { searchParams } = new URL(req.url);
const cursor = searchParams.get("cursor");
const channelId = searchParams.get("channelId");
if (!profile) {
return new NextResponse("Unauthorized", { status: 401 });
}
if (!channelId) {
return new NextResponse("Channel ID missing", { status: 400 });
}
let messages: Message[] = [];
if (cursor) {
messages = await db.message.findMany({
take: MESSAGES_BATCH,
skip: 1,
cursor: {
id: cursor,
},
where: {
channelId,
},
include: {
member: {
include: {
profile: true,
}
}
},
orderBy: {
createdAt: "desc",
},
});
} else {
messages = await db.message.findMany({
take: MESSAGES_BATCH,
where: {
channelId,
},
include: {
member: {
include: {
profile: true,
}
}
},
orderBy: {
createdAt: "desc",
},
});
}
let nextCursor = null;
if (messages.length === MESSAGES_BATCH) {
nextCursor = messages[MESSAGES_BATCH - 1].id;
}
return NextResponse.json({
items: messages,
nextCursor,
});
} catch (error) {
console.log("[MESSAGES_GET]", error);
return new NextResponse("Internal Error", { status: 500 })
}
}

View File

@ -7,6 +7,7 @@ import { cn } from '@/lib/utils'
import { ThemeProvider } from '@/components/providers/theme-provider' import { ThemeProvider } from '@/components/providers/theme-provider'
import { ModalProvider } from '@/components/providers/modal-provider' import { ModalProvider } from '@/components/providers/modal-provider'
import { SocketProvider } from '@/components/providers/socket-provider' import { SocketProvider } from '@/components/providers/socket-provider'
import { QueryProvider } from '@/components/providers/query-provider'
const font = Open_Sans({ subsets: ['latin'] }) const font = Open_Sans({ subsets: ['latin'] })
@ -35,7 +36,9 @@ export default function RootLayout({
> >
<SocketProvider> <SocketProvider>
<ModalProvider/> <ModalProvider/>
{children} <QueryProvider>
{children}
</QueryProvider>
</SocketProvider> </SocketProvider>
</ThemeProvider> </ThemeProvider>
</body> </body>

View File

@ -0,0 +1,96 @@
"use client";
import { Member, Message, Profile } from "@prisma/client";
import { ChatWelcome } from "./chat-welcome";
import { useChatQuery } from "@/hooks/use-chat-query";
import { Loader2, ServerCrash } from "lucide-react";
import { Fragment } from "react";
type MessageWithMemberWithProfile = Message & {
member: Member & {
profile: Profile
}
}
interface ChatMessagesProps {
name: string;
member: Member;
chatId: string;
apiUrl: string;
socketUrl: string;
socketQuery: Record<string, string>;
paramKey: "channelId" | "conversationId";
paramValue: string;
type: "channel" | "conversation";
}
export const ChatMessages = ({
name,
member,
chatId,
apiUrl,
socketUrl,
socketQuery,
paramKey,
paramValue,
type,
}: ChatMessagesProps) => {
const queryKey = `chat:${chatId}`;
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
status,
} = useChatQuery({
queryKey,
apiUrl,
paramKey,
paramValue,
})
if (status === "pending") {
return (
<div className="flex flex-col flex-1 justify-center items-center">
<Loader2 className="h-7 w-7 text-zinc-500 animate-spin my-4"/>
<p className="text-xs text-zinc-500 dark:text-zinc-400">
Loading Messages...
</p>
</div>
)
}
if (status === "error") {
return (
<div className="flex flex-col flex-1 justify-center items-center">
<ServerCrash className="h-7 w-7 text-zinc-500 my-4"/>
<p className="text-xs text-zinc-500 dark:text-zinc-400">
Something went wrong!
</p>
</div>
)
}
return (
<div className="flex-1 flex flex-col py-4 overflow-y-auto">
<div className="flex-1"/>
<ChatWelcome
type={type}
name={name}
/>
<div className="flex flex-col-reverse mt-auto">
{data?.pages.map((group, i) => (
<Fragment key={i}>
{group.items.map((message: MessageWithMemberWithProfile) => (
<div key={message.id}>
{message.content}
</div>
))}
</Fragment>
))}
</div>
</div>
)
}

View File

@ -0,0 +1,28 @@
import { Hash } from "lucide-react";
interface ChatWelcomeProps {
name: string;
type: "channel" | "conversation";
}
export const ChatWelcome = ({
name,
type,
}: ChatWelcomeProps) => {
return (
<div className="space-y-2 px-4 mb-4">
{type === "channel" && (
<div className="h-[75px] w-[75px] rounded-full bg-zinc-500 dark:bg-zinc-700 flex items-center justify-center">
<Hash className="h-12 w-12 text-white"/>
</div>
)}
<p className="text-xl md:text-3l font-bold">
{type === "channel" ? "Welcome to #" : ""}{name}
</p>
<p className="text-zinc-600 dark:text-zinc-400 text-sm">
{type === "channel" ? `This is the start of the #${name} channel.` : `This is the start of your conversation with ${name}`}
</p>
</div>
)
}

View File

@ -0,0 +1,21 @@
"use client"
import {
QueryClient,
QueryClientProvider,
}from "@tanstack/react-query";
import { useState } from "react";
export const QueryProvider = ({
children
}: {
children: React.ReactNode;
}) => {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}

54
hooks/use-chat-query.ts Normal file
View File

@ -0,0 +1,54 @@
import qs from "query-string";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useSocket } from "@/components/providers/socket-provider";
interface ChatQueryProps {
queryKey: string;
apiUrl: string;
paramKey: "channelId" | "conversationId";
paramValue: string;
};
export const useChatQuery = ({
queryKey,
apiUrl,
paramKey,
paramValue
}: ChatQueryProps) => {
const { isConnected } = useSocket();
const fetchMessages = async ({ pageParam = undefined }) => {
const url = qs.stringifyUrl({
url: apiUrl,
query: {
cursor: pageParam,
[paramKey]: paramValue,
}
}, { skipNull: true });
const res = await fetch(url);
return res.json();
};
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: [queryKey],
queryFn: fetchMessages,
getNextPageParam: (lastPage) => lastPage?.nextCursor,
refetchInterval: isConnected ? false : 1000,
});
return {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
status,
};
}