Skip to content

Commit

Permalink
Merge pull request #7 from davitJabushanuri/release/1.9.6
Browse files Browse the repository at this point in the history
Release/1.9.6
  • Loading branch information
davitJabushanuri committed Mar 26, 2024
2 parents 9fd8994 + 4212b58 commit 56f3abe
Show file tree
Hide file tree
Showing 14 changed files with 290 additions and 128 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "chirp",
"description": "Chirp is a social media app built with Next.js, Prisma, and Supabase",
"version": "1.9.5",
"version": "1.9.6",
"private": true,
"scripts": {
"start": "next start",
Expand Down
19 changes: 18 additions & 1 deletion src/app/api/messages/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import { prisma } from "@/lib/prisma";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const conversation_id = searchParams.get("conversation_id") as string;
const cursorQuery = searchParams.get("cursor") || undefined;
const take = Number(searchParams.get("limit")) || 20;

const skip = cursorQuery ? 1 : 0;
const cursor = cursorQuery ? { id: cursorQuery } : undefined;

const messageSchema = z.string();
const zod = messageSchema.safeParse(conversation_id);
Expand All @@ -16,12 +21,24 @@ export async function GET(request: Request) {

try {
const chat = await prisma.message.findMany({
skip,
take,
cursor,
where: {
conversation_id: conversation_id,
},

orderBy: {
created_at: "desc",
},
});

return NextResponse.json(chat, { status: 200 });
const nextId = chat.length < take ? undefined : chat[chat.length - 1].id;

return NextResponse.json({
chat,
nextId,
});
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
Expand Down
59 changes: 28 additions & 31 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import { AuthFlow } from "@/features/auth";
import { MobileTweetButton } from "@/features/create-tweet";
import { MobileNavbar } from "@/features/navbar";
import { Sidebar } from "@/features/sidebar";
import { NextAuthProvider } from "@/utils/next-auth-provider";
import { ReactQueryProvider } from "@/utils/react-query-provider";
import { AppProviders } from "@/providers";

import { Hamburger } from "./hamburger";
import { JoinTwitter } from "./join-twitter";
Expand All @@ -35,40 +34,38 @@ export default async function RootLayout({
lang="en"
>
<body suppressHydrationWarning={true}>
<NextAuthProvider>
<ReactQueryProvider>
<div className="layout">
<MobileNavbar />
<div className="fixed bottom-20 right-4 z-fixed sm:hidden">
<MobileTweetButton />
</div>
<AppProviders>
<div className="layout">
<MobileNavbar />
<div className="fixed bottom-20 right-4 z-fixed sm:hidden">
<MobileTweetButton />
</div>

<Sidebar />
<Sidebar />

<main aria-label="Home timeline" id="home-timeline">
{children}
</main>
<main aria-label="Home timeline" id="home-timeline">
{children}
</main>

<Aside />
<Aside />

<ToastContainer
position="bottom-center"
autoClose={2000}
hideProgressBar={true}
transition={Slide}
closeButton={false}
closeOnClick={true}
className={styles.container}
toastClassName={styles.toast}
role="alert"
/>
<ToastContainer
position="bottom-center"
autoClose={2000}
hideProgressBar={true}
transition={Slide}
closeButton={false}
closeOnClick={true}
className={styles.container}
toastClassName={styles.toast}
role="alert"
/>

<AuthFlow />
<JoinTwitter />
<Hamburger />
</div>
</ReactQueryProvider>
</NextAuthProvider>
<AuthFlow />
<JoinTwitter />
<Hamburger />
</div>
</AppProviders>
</body>
</html>
);
Expand Down
12 changes: 10 additions & 2 deletions src/features/messages/api/get-chat.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import axios from "axios";

export const getChat = async (conversation_id: string | undefined) => {
export const getChat = async ({
conversation_id,
pageParam,
limit,
}: {
conversation_id: string | undefined;
pageParam: string | unknown;
limit: number;
}) => {
try {
const { data } = await axios.get(
`/api/messages/chat?conversation_id=${conversation_id}`,
`/api/messages/chat?conversation_id=${conversation_id}&cursor=${pageParam}&limit=${limit}`,
);
return data;
} catch (error: any) {
Expand Down
146 changes: 91 additions & 55 deletions src/features/messages/components/chat.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { usePathname } from "next/navigation";
import { useSession } from "next-auth/react";
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useInView } from "react-intersection-observer";

import { Button } from "@/components/elements/button";
Expand All @@ -15,95 +16,130 @@ import { Message } from "./message";

export type status = "sending" | "sent" | "seen" | "failed";

export const Chat = ({
conversation_id,
}: {
conversation_id: string | undefined;
}) => {
const { ref, inView } = useInView({
threshold: 0,
});
export const Chat = memo(() => {
const pathname = usePathname();
const conversation_id = pathname?.split("/")[2];

const { data: session } = useSession();
const {
data,
isLoading,
isError,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
} = useChat(conversation_id);

const isLastMessageSender = useMemo(
() => data?.pages.at(-1)?.chat?.at(-1)?.sender_id === session?.user?.id,
[data, session],
);

const anchorRef = useRef<HTMLDivElement | null>(null);
const [toast, setToast] = useState<
"new message" | "scroll to bottom" | "none"
>("none");

const [scrolledToBottom, setScrolledToBottom] = useState(false);
const [displayNewMessageToast, setDisplayNewMessageToast] = useState(false);
const { ref: firstMessageRef } = useInView({
initialInView: false,
onChange(inView) {
if (inView && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
setToast("scroll to bottom");
}
},
});

const { data: session } = useSession();
const { data: chat, isLoading, isError } = useChat(conversation_id);
const { ref: lastMessageRef, inView: lastMessageInView } = useInView({
initialInView: true,
});

const handleToastClick = useCallback(() => {
scrollIntoView({
element: anchorRef.current,
});
setToast("none");
}, []);

useSocketEvents(conversation_id);

useEffect(() => {
if (inView) {
setScrolledToBottom(true);
} else {
setScrolledToBottom(false);
}
}, [inView]);
if (lastMessageInView) {
setToast("none");
} else setToast("scroll to bottom");
}, [lastMessageInView]);

useLayoutEffect(() => {
if (!scrolledToBottom) {
useEffect(() => {
if (lastMessageInView) {
scrollIntoView({
element: anchorRef.current,
behavior: "instant",
});
} else {
if (
chat &&
chat?.length > 0 &&
chat[chat.length - 1]?.sender_id !== session?.user?.id
)
setDisplayNewMessageToast(true);
} else if (!isLastMessageSender) {
setToast("new message");
}
}, [chat]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);

if (isLoading) return <LoadingSpinner />;

if (isError) return <TryAgain />;

return (
<div className="p-[1em_1em_0]">
{chat?.map((message, index) => {
return (
<div key={message?.id} ref={index === chat.length - 1 ? ref : null}>
<Message
message={message}
show_status={
index === chat.length - 1 &&
message.sender_id === session?.user?.id
{isFetchingNextPage && <LoadingSpinner />}

{data?.pages?.map((page, pageIndex) => {
return page?.chat?.map((message, messageIndex) => {
return (
<div
ref={
messageIndex === 0 && pageIndex === 0
? firstMessageRef
: messageIndex === page.chat.length - 1 &&
pageIndex === data.pages.length - 1
? lastMessageRef
: null
}
/>
</div>
);
key={message.id}
>
<Message
show_status={
message.sender_id === session?.user?.id &&
messageIndex === page.chat.length - 1 &&
pageIndex === data.pages.length - 1
}
message={message}
/>
</div>
);
});
})}
<div id="anchor" ref={anchorRef} />
{!scrolledToBottom && !displayNewMessageToast && (

{toast === "new message" && (
<Button
onClick={() => {
scrollIntoView({
element: anchorRef.current,
behavior: "smooth",
});
}}
className="shadow-main absolute bottom-[5rem] right-[1.6rem] bg-background fill-primary-100 px-[1em] py-[0.5em] hover:bg-neutral-500 focus-visible:bg-neutral-500 focus-visible:outline-secondary-100/50 active:bg-neutral-600"
className="shadow-main absolute bottom-[5rem] left-[50%] translate-x-[-50%] bg-background px-[1em] py-[0.5em] text-milli font-bold text-primary-100 hover:bg-neutral-500 focus-visible:bg-neutral-500 focus-visible:outline-secondary-100/50 active:bg-neutral-600"
onClick={handleToastClick}
>
<ArrowDownIcon />
↓ New messages
</Button>
)}

{displayNewMessageToast && (
{toast === "scroll to bottom" && (
<Button
className="shadow-main absolute bottom-[5rem] left-[50%] translate-x-[-50%] bg-background px-[1em] py-[0.5em] text-milli font-bold text-primary-100 hover:bg-neutral-500 focus-visible:bg-neutral-500 focus-visible:outline-secondary-100/50 active:bg-neutral-600"
onClick={() => {
scrollIntoView({
element: anchorRef.current,
behavior: "smooth",
});
setDisplayNewMessageToast(false);
}}
className="shadow-main absolute bottom-[5rem] right-[1.6rem] bg-background fill-primary-100 px-[1em] py-[0.5em] hover:bg-neutral-500 focus-visible:bg-neutral-500 focus-visible:outline-secondary-100/50 active:bg-neutral-600"
>
↓ New messages
<ArrowDownIcon />
</Button>
)}
</div>
);
};
});

Chat.displayName = "Chat";
6 changes: 3 additions & 3 deletions src/features/messages/components/conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ export const Conversation = () => {

useEffect(() => {
socket.auth = { conversation_id: id };
socket.connect();
socket?.connect();

return () => {
socket.disconnect();
socket?.disconnect();
};
}, [id]);

Expand Down Expand Up @@ -65,7 +65,7 @@ export const Conversation = () => {
<div ref={ref}>
<ConversationMemberDetails user={conversationMember} />
</div>
<Chat conversation_id={conversation?.id} />
<Chat />
</div>

<MessageInput
Expand Down
Loading

0 comments on commit 56f3abe

Please sign in to comment.