Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release/1.9.6 #7

Merged
merged 25 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a223fa8
chore: bump version number to 1.9.5
davitJabushanuri Mar 8, 2024
f796b21
refactor: chat API endpoint to support infinite scroll
davitJabushanuri Mar 9, 2024
ee2314b
refactor: use-get-chat hook and get-chat function to support infinite…
davitJabushanuri Mar 9, 2024
9dbebdd
fix: check if the chat array has messages before retrieving the last one
davitJabushanuri Mar 9, 2024
bf026f6
refactor: chat component to use memo to avoid extra rerenders and sup…
davitJabushanuri Mar 9, 2024
130822f
refactor: use-get-chat hook to sort messages correctly
davitJabushanuri Mar 13, 2024
8845f4e
refactor: reverse pages with tanstack query built in function
davitJabushanuri Mar 13, 2024
026a209
refactor: message-input component to optimistically update chat infin…
davitJabushanuri Mar 13, 2024
edc38bb
feat: create socket context
davitJabushanuri Mar 15, 2024
48ca776
refactor: order messages by descending
davitJabushanuri Mar 15, 2024
8a371a0
refactor: check if chat exists before reversing it
davitJabushanuri Mar 15, 2024
951a5d8
chore: move next-aut and react query providers to providers folder
davitJabushanuri Mar 15, 2024
d0819fc
feat: create AppProviders component and wrap the whole app with it
davitJabushanuri Mar 15, 2024
f4ee06a
refactor: update use-socket-events hook to work with infinite query
davitJabushanuri Mar 18, 2024
825ba59
chore: get rid of socket context
davitJabushanuri Mar 20, 2024
1b1f0cb
chore: update socket io config
davitJabushanuri Mar 20, 2024
e627f6e
chore: remove socket context from providers
davitJabushanuri Mar 20, 2024
a527c67
Merge branch 'hotfix/1.9.5' into develop
davitJabushanuri Mar 24, 2024
3e2c77b
fix: target last message correctly, delete console logs
davitJabushanuri Mar 25, 2024
4e60fdd
feat: add infinite scroll, scroll to bottom and new message toasts to…
davitJabushanuri Mar 26, 2024
db89ced
refactor: reverse data in place and return it
davitJabushanuri Mar 26, 2024
fe672cc
refactor: message component to display status only on the last one
davitJabushanuri Mar 26, 2024
8e0ca26
Merge branch 'develop' into feature/realtime-chat-improvements
davitJabushanuri Mar 26, 2024
bec1e31
Merge pull request #5 from davitJabushanuri/feature/realtime-chat-imp…
davitJabushanuri Mar 26, 2024
4212b58
chore: bump version number to 1.9.6
davitJabushanuri Mar 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading