Skip to content

Commit

Permalink
chat complete
Browse files Browse the repository at this point in the history
  • Loading branch information
cieko committed Mar 11, 2024
1 parent 80864d4 commit 3377e56
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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) {
Expand All @@ -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 <div className="flex flex-col h-full">
<ChatHeader
imageUrl={otherMember.profile.imageUrl}
name={otherMember.profile.name}
serverId={params.serverId}
type="conversation"
/>
</div>;
const otherMember =
memberOne.profileId === profile.id ? memberTwo : memberOne;

return (
<div className="flex flex-col h-full">
<ChatHeader
imageUrl={otherMember.profile.imageUrl}
name={otherMember.profile.name}
serverId={params.serverId}
type="conversation"
/>
{/* {searchParams.video && (
<MediaRoom
chatId={conversation.id}
video={true}
audio={true}
/>
)} */}
{!searchParams.video && (
<>
<ChatMessages
member={currentMember}
name={otherMember.profile.name}
chatId={conversation.id}
type="conversation"
apiUrl="/api/direct-messages"
paramKey="conversationId"
paramValue={conversation.id}
socketUrl="/api/socket/direct-messages"
socketQuery={{
conversationId: conversation.id,
}}
/>
<ChatInput
name={otherMember.profile.name}
type="conversation"
apiUrl="/api/socket/direct-messages"
query={{
conversationId: conversation.id,
}}
/>
</>
)}
</div>
);
};

export default MemberPage;
83 changes: 83 additions & 0 deletions app/api/direct-messages/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
20 changes: 20 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion components/chat/chat-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
34 changes: 30 additions & 4 deletions components/chat/chat-messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand Down Expand Up @@ -44,6 +45,9 @@ export const ChatMessages = ({
const addKey = `chat:${chatId}:messages`;
const updateKey = `chat:${chatId}:messages:update`;

const chatRef = useRef<ElementRef<'div'>>(null);
const bottomRef = useRef<ElementRef<'div'>>(null);

const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } =
useChatQuery({
queryKey,
Expand All @@ -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") {
Expand All @@ -76,9 +87,23 @@ export const ChatMessages = ({
}

return (
<div className="flex-1 flex flex-col py-4 overflow-y-auto">
<div className="flex-1" />
<ChatWelcome type={type} name={name} />
<div ref={chatRef} className="flex-1 flex flex-col py-4 overflow-y-auto">
{!hasNextPage && <div className="flex-1" />}
{!hasNextPage && <ChatWelcome type={type} name={name} />}
{hasNextPage && (
<div className="flex justify-center">
{isFetchingNextPage ? (
<Loader2 className="h-6 w-6 animate-spin my-4 text-amber-500" />
) : (
<button
className="text-amber-400 hover:text-amber-300 text-xs transition"
onClick={() => fetchNextPage()}
>
Load previous messages
</button>
)}
</div>
)}
<div className="flex flex-col-reverse mt-auto">
{data?.pages?.map((group, i) => {
return (
Expand All @@ -104,6 +129,7 @@ export const ChatMessages = ({
);
})}
</div>
<div ref={bottomRef} />
</div>
);
};
63 changes: 63 additions & 0 deletions hooks/use-chat-scroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useEffect, useState } from "react";

type ChatScrollProps = {
chatRef: React.RefObject<HTMLDivElement>;
bottomRef: React.RefObject<HTMLDivElement>;
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]);
}

0 comments on commit 3377e56

Please sign in to comment.