From 3377e568873aafb1a4265a95d28967fe2a9b78dd Mon Sep 17 00:00:00 2001 From: cieko Date: Mon, 11 Mar 2024 16:33:17 +0530 Subject: [PATCH] chat complete --- .../conversations/[memberId]/page.tsx | 69 ++++++++++++--- app/api/direct-messages/route.ts | 83 +++++++++++++++++++ app/globals.css | 20 +++++ components/chat/chat-item.tsx | 2 +- components/chat/chat-messages.tsx | 34 +++++++- hooks/use-chat-scroll.ts | 63 ++++++++++++++ 6 files changed, 253 insertions(+), 18 deletions(-) create mode 100644 app/api/direct-messages/route.ts create mode 100644 hooks/use-chat-scroll.ts diff --git a/app/(main)/(routes)/servers/[serverId]/conversations/[memberId]/page.tsx b/app/(main)/(routes)/servers/[serverId]/conversations/[memberId]/page.tsx index 7bf3b7f..4acd703 100644 --- a/app/(main)/(routes)/servers/[serverId]/conversations/[memberId]/page.tsx +++ b/app/(main)/(routes)/servers/[serverId]/conversations/[memberId]/page.tsx @@ -1,4 +1,6 @@ import ChatHeader from "@/components/chat/chat-header"; +import { ChatInput } from "@/components/chat/chat-input"; +import { ChatMessages } from "@/components/chat/chat-messages"; import { getOrCreateConversation } from "@/lib/conversation"; import { currentProfile } from "@/lib/current-profile"; import { db } from "@/lib/db"; @@ -10,9 +12,12 @@ interface MemberIdPageProps { memberId: string; serverId: string; }; + searchParams: { + video?: boolean; + }; } -const MemberPage = async ({ params }: MemberIdPageProps) => { +const MemberPage = async ({ params, searchParams }: MemberIdPageProps) => { const profile = await currentProfile(); if (!profile) { @@ -33,23 +38,61 @@ const MemberPage = async ({ params }: MemberIdPageProps) => { return redirect("/"); } - const conversation = await getOrCreateConversation(currentMember.id, params.memberId); + const conversation = await getOrCreateConversation( + currentMember.id, + params.memberId + ); if (!conversation) { - return redirect(`/servers/${params.serverId}`) + return redirect(`/servers/${params.serverId}`); } const { memberOne, memberTwo } = conversation; - const otherMember = memberOne.profileId === profile.id ? memberTwo : memberOne; - - return
- -
; + const otherMember = + memberOne.profileId === profile.id ? memberTwo : memberOne; + + return ( +
+ + {/* {searchParams.video && ( + + )} */} + {!searchParams.video && ( + <> + + + + )} +
+ ); }; export default MemberPage; diff --git a/app/api/direct-messages/route.ts b/app/api/direct-messages/route.ts new file mode 100644 index 0000000..432d1f4 --- /dev/null +++ b/app/api/direct-messages/route.ts @@ -0,0 +1,83 @@ +import { NextResponse } from "next/server"; +import { DirectMessage } from "@prisma/client"; + +import { currentProfile } from "@/lib/current-profile"; +import { db } from "@/lib/db"; + +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 conversationId = searchParams.get("conversationId"); + + if (!profile) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + if (!conversationId) { + return new NextResponse("Conversation ID missing", { status: 400 }); + } + + let messages: DirectMessage[] = []; + + if (cursor) { + messages = await db.directMessage.findMany({ + take: MESSAGES_BATCH, + skip: 1, + cursor: { + id: cursor, + }, + where: { + conversationId, + }, + include: { + member: { + include: { + profile: true, + } + } + }, + orderBy: { + createdAt: "desc", + } + }) + } else { + messages = await db.directMessage.findMany({ + take: MESSAGES_BATCH, + where: { + conversationId, + }, + 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("[DIRECT_MESSAGES_GET]", error); + return new NextResponse("Internal Error", { status: 500 }); + } +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index fe6263d..271d6e3 100644 --- a/app/globals.css +++ b/app/globals.css @@ -79,4 +79,24 @@ body, body { @apply bg-background text-foreground; } +} + +/* width */ +::-webkit-scrollbar { + width: 4px; +} + +/* Track */ +::-webkit-scrollbar-track { + box-shadow: inset 0 0 5px #00000000; +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background: rgb(128, 36, 0); +} + +/* Handle on hover */ +::-webkit-scrollbar-thumb:hover { + background: #ff9011e7; } \ No newline at end of file diff --git a/components/chat/chat-item.tsx b/components/chat/chat-item.tsx index 05a5a31..223fee6 100644 --- a/components/chat/chat-item.tsx +++ b/components/chat/chat-item.tsx @@ -176,7 +176,7 @@ export const ChatItem = ({ className={cn( "text-xl text-[#ffbd80]", deleted && - "italic text-amber-400 text-md mt-1" + "italic text-amber-400/65 text-md mt-1" )} > {content} diff --git a/components/chat/chat-messages.tsx b/components/chat/chat-messages.tsx index 14e3233..b41b712 100644 --- a/components/chat/chat-messages.tsx +++ b/components/chat/chat-messages.tsx @@ -5,9 +5,10 @@ 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"; +import { Fragment, useRef, ElementRef } from "react"; import { ChatItem } from "./chat-item"; import { useChatSocket } from "@/hooks/use-chat-socket"; +import { useChatScroll } from "@/hooks/use-chat-scroll"; type MessageWithMemberWithProfile = Message & { member: Member & { @@ -44,6 +45,9 @@ export const ChatMessages = ({ const addKey = `chat:${chatId}:messages`; const updateKey = `chat:${chatId}:messages:update`; + const chatRef = useRef>(null); + const bottomRef = useRef>(null); + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } = useChatQuery({ queryKey, @@ -55,6 +59,13 @@ export const ChatMessages = ({ queryKey, addKey, updateKey, + }); + useChatScroll({ + chatRef, + bottomRef, + loadMore: fetchNextPage, + shouldLoadMore: !isFetchingNextPage && !!hasNextPage, + count: data?.pages?.[0]?.items?.length ?? 0, }) if (status === "pending") { @@ -76,9 +87,23 @@ export const ChatMessages = ({ } return ( -
-
- +
+ {!hasNextPage &&
} + {!hasNextPage && } + {hasNextPage && ( +
+ {isFetchingNextPage ? ( + + ) : ( + + )} +
+ )}
{data?.pages?.map((group, i) => { return ( @@ -104,6 +129,7 @@ export const ChatMessages = ({ ); })}
+
); }; diff --git a/hooks/use-chat-scroll.ts b/hooks/use-chat-scroll.ts new file mode 100644 index 0000000..fce5693 --- /dev/null +++ b/hooks/use-chat-scroll.ts @@ -0,0 +1,63 @@ +import { useEffect, useState } from "react"; + +type ChatScrollProps = { + chatRef: React.RefObject; + bottomRef: React.RefObject; + shouldLoadMore: boolean; + loadMore: () => void; + count: number; +}; + +export const useChatScroll = ({ + chatRef, + bottomRef, + shouldLoadMore, + loadMore, + count, +}: ChatScrollProps) => { + const [hasInitialized, setHasInitialized] = useState(false); + + useEffect(() => { + const topDiv = chatRef?.current; + + const handleScroll = () => { + const scrollTop = topDiv?.scrollTop; + + if (scrollTop === 0 && shouldLoadMore) { + loadMore() + } + }; + + topDiv?.addEventListener("scroll", handleScroll); + + return () => { + topDiv?.removeEventListener("scroll", handleScroll); + } + }, [shouldLoadMore, loadMore, chatRef]); + + useEffect(() => { + const bottomDiv = bottomRef?.current; + const topDiv = chatRef.current; + const shouldAutoScroll = () => { + if (!hasInitialized && bottomDiv) { + setHasInitialized(true); + return true; + } + + if (!topDiv) { + return false; + } + + const distanceFromBottom = topDiv.scrollHeight - topDiv.scrollTop - topDiv.clientHeight; + return distanceFromBottom <= 100; + } + + if (shouldAutoScroll()) { + setTimeout(() => { + bottomRef.current?.scrollIntoView({ + behavior: "smooth", + }); + }, 100); + } + }, [bottomRef, chatRef, count, hasInitialized]); +} \ No newline at end of file