added chat messages component and query provider
This commit is contained in:
parent
55ad18e04a
commit
70141f46fd
@ -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
83
app/api/messages/route.ts
Normal 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 })
|
||||||
|
}
|
||||||
|
}
|
@ -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/>
|
||||||
|
<QueryProvider>
|
||||||
{children}
|
{children}
|
||||||
|
</QueryProvider>
|
||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
|
96
components/chat/chat-messages.tsx
Normal file
96
components/chat/chat-messages.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
28
components/chat/chat-welcome.tsx
Normal file
28
components/chat/chat-welcome.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
21
components/providers/query-provider.tsx
Normal file
21
components/providers/query-provider.tsx
Normal 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
54
hooks/use-chat-query.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user