From a223fa8dbe7907df1a5ee315bc62ff2a66fadcd0 Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Fri, 8 Mar 2024 17:48:58 +0400 Subject: [PATCH 01/21] chore: bump version number to 1.9.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c97cfc..0a534b1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "chirp", "description": "Chirp is a social media app built with Next.js, Prisma, and Supabase", - "version": "1.9.4", + "version": "1.9.5", "private": true, "scripts": { "start": "next start", From f796b21ecbe0017a5dd09667eaf28b82f8469e81 Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Sat, 9 Mar 2024 19:18:49 +0400 Subject: [PATCH 02/21] refactor: chat API endpoint to support infinite scroll --- src/app/api/messages/chat/route.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/app/api/messages/chat/route.ts b/src/app/api/messages/chat/route.ts index 9a72001..2a7e9f3 100644 --- a/src/app/api/messages/chat/route.ts +++ b/src/app/api/messages/chat/route.ts @@ -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); @@ -16,12 +21,20 @@ export async function GET(request: Request) { try { const chat = await prisma.message.findMany({ + skip, + take, + cursor, where: { conversation_id: conversation_id, }, }); - 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 }); } From ee2314bc078fa400fbb0c178462eeabb854e4796 Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Sat, 9 Mar 2024 19:20:10 +0400 Subject: [PATCH 03/21] refactor: use-get-chat hook and get-chat function to support infinite scroll --- src/features/messages/api/get-chat.ts | 12 ++++++++-- src/features/messages/hooks/use-get-chat.ts | 26 +++++++++++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/features/messages/api/get-chat.ts b/src/features/messages/api/get-chat.ts index 15ead9f..9916d25 100644 --- a/src/features/messages/api/get-chat.ts +++ b/src/features/messages/api/get-chat.ts @@ -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) { diff --git a/src/features/messages/hooks/use-get-chat.ts b/src/features/messages/hooks/use-get-chat.ts index d8d1261..5cd77c3 100644 --- a/src/features/messages/hooks/use-get-chat.ts +++ b/src/features/messages/hooks/use-get-chat.ts @@ -1,13 +1,31 @@ -import { useQuery } from "@tanstack/react-query"; +import { useInfiniteQuery } from "@tanstack/react-query"; import { getChat } from "../api/get-chat"; import { IMessage } from "../types"; +interface IInfiniteChat { + nextId: string; + chat: IMessage[]; +} + export const useChat = (conversation_id: string | undefined) => { - return useQuery({ + const response = useInfiniteQuery({ queryKey: ["chat", conversation_id], - queryFn: async () => { - return getChat(conversation_id); + queryFn: ({ pageParam }) => { + return getChat({ conversation_id, pageParam, limit: 20 }); + }, + initialPageParam: "", + getNextPageParam: (lastPage) => { + return lastPage?.nextId; }, }); + + return { + data: response.data?.pages.flatMap((page) => page.chat), + isLoading: response.isLoading, + isError: response.isError, + isFetchingNextPage: response.isFetchingNextPage, + fetchNextPage: response.fetchNextPage, + hasNextPage: response.hasNextPage, + }; }; From 9dbebdd449b00ffdc16b767494847380131d875d Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Sat, 9 Mar 2024 19:21:34 +0400 Subject: [PATCH 04/21] fix: check if the chat array has messages before retrieving the last one --- src/features/messages/components/message-input.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/messages/components/message-input.tsx b/src/features/messages/components/message-input.tsx index a0c3a2e..689c234 100644 --- a/src/features/messages/components/message-input.tsx +++ b/src/features/messages/components/message-input.tsx @@ -63,8 +63,8 @@ export const MessageInput = ({ "chat", conversation_id, ]); - if (!chat) return; - const lastMessage = chat.at(-1); + if (!chat || chat.length === 0) return; + const lastMessage = chat[chat.length - 1]; if (lastMessage?.sender_id === sender_id) return; From bf026f6ce85f924817e931d1fcd2b52a5fdaa012 Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Sat, 9 Mar 2024 19:23:45 +0400 Subject: [PATCH 05/21] refactor: chat component to use memo to avoid extra rerenders and support infinite scroll --- src/features/messages/components/conversation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/messages/components/conversation.tsx b/src/features/messages/components/conversation.tsx index 85357c6..ad743f8 100644 --- a/src/features/messages/components/conversation.tsx +++ b/src/features/messages/components/conversation.tsx @@ -65,7 +65,7 @@ export const Conversation = () => {
- + Date: Wed, 13 Mar 2024 20:46:27 +0400 Subject: [PATCH 06/21] refactor: use-get-chat hook to sort messages correctly --- src/features/messages/hooks/use-get-chat.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/features/messages/hooks/use-get-chat.ts b/src/features/messages/hooks/use-get-chat.ts index 5cd77c3..c2b27b2 100644 --- a/src/features/messages/hooks/use-get-chat.ts +++ b/src/features/messages/hooks/use-get-chat.ts @@ -3,7 +3,7 @@ import { useInfiniteQuery } from "@tanstack/react-query"; import { getChat } from "../api/get-chat"; import { IMessage } from "../types"; -interface IInfiniteChat { +export interface IInfiniteChat { nextId: string; chat: IMessage[]; } @@ -20,8 +20,14 @@ export const useChat = (conversation_id: string | undefined) => { }, }); + const chat = response?.data?.pages?.reduce((acc, page) => { + const reversedChat = [...page.chat].reverse(); + + return [...reversedChat, ...acc]; + }, [] as IMessage[]); + return { - data: response.data?.pages.flatMap((page) => page.chat), + data: chat, isLoading: response.isLoading, isError: response.isError, isFetchingNextPage: response.isFetchingNextPage, From 8845f4e72721653258842411186936d57ab8d50b Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Wed, 13 Mar 2024 21:24:08 +0400 Subject: [PATCH 07/21] refactor: reverse pages with tanstack query built in function --- src/features/messages/hooks/use-get-chat.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/features/messages/hooks/use-get-chat.ts b/src/features/messages/hooks/use-get-chat.ts index c2b27b2..56b5cd3 100644 --- a/src/features/messages/hooks/use-get-chat.ts +++ b/src/features/messages/hooks/use-get-chat.ts @@ -18,12 +18,17 @@ export const useChat = (conversation_id: string | undefined) => { getNextPageParam: (lastPage) => { return lastPage?.nextId; }, + + select: (data) => ({ + pages: [...data.pages].reverse(), + pageParams: [...data.pageParams].reverse(), + }), }); const chat = response?.data?.pages?.reduce((acc, page) => { const reversedChat = [...page.chat].reverse(); - return [...reversedChat, ...acc]; + return [...acc, ...reversedChat]; }, [] as IMessage[]); return { From 026a20918ce301e21ed8ab493243359bf1747738 Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Wed, 13 Mar 2024 23:42:50 +0400 Subject: [PATCH 08/21] refactor: message-input component to optimistically update chat infinite query --- .../messages/components/message-input.tsx | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/src/features/messages/components/message-input.tsx b/src/features/messages/components/message-input.tsx index 689c234..777c179 100644 --- a/src/features/messages/components/message-input.tsx +++ b/src/features/messages/components/message-input.tsx @@ -1,5 +1,5 @@ import { createId } from "@paralleldrive/cuid2"; -import { useQueryClient } from "@tanstack/react-query"; +import { InfiniteData, useQueryClient } from "@tanstack/react-query"; import Image from "next/image"; import { useRef, useState } from "react"; @@ -13,7 +13,7 @@ import { IChosenImages } from "@/features/create-tweet"; import { socket } from "@/lib/socket-io"; import { SendIcon } from "../assets/send-icon"; -import { IMessage } from "../types"; +import { IInfiniteChat } from "../hooks/use-get-chat"; import styles from "./styles/message-input.module.scss"; @@ -59,12 +59,12 @@ export const MessageInput = ({ }; const onFocus = () => { - const chat = queryClient.getQueryData([ + const chat = queryClient.getQueryData>([ "chat", conversation_id, ]); - if (!chat || chat.length === 0) return; - const lastMessage = chat[chat.length - 1]; + + const lastMessage = chat?.pages?.at(-1)?.chat?.at(-1); if (lastMessage?.sender_id === sender_id) return; @@ -125,9 +125,36 @@ export const MessageInput = ({ status: "sending", }); - queryClient.setQueryData(["chat", conversation_id], (oldData: any) => { - return [...oldData, message]; - }); + queryClient.setQueryData( + ["chat", conversation_id], + (oldData: InfiniteData) => { + const lastPage = oldData.pages?.at(0); + if (lastPage?.chat && lastPage.chat.length >= 20) { + console.log("hello"); + return { + ...oldData, + pages: [ + { + chat: [message], + nextId: message.id, + }, + ...oldData.pages, + ], + }; + } else { + return { + ...oldData, + pages: [ + { + chat: [message, ...(lastPage?.chat ?? [])], + nextId: lastPage?.nextId, + }, + ...oldData.pages.slice(1), + ], + }; + } + }, + ); socket.emit("message", { id, From edc38bbe00cafbf5ec7267008c921d3e88c60f6c Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Fri, 15 Mar 2024 22:36:11 +0400 Subject: [PATCH 09/21] feat: create socket context --- src/contexts/socket.tsx | 60 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/contexts/socket.tsx diff --git a/src/contexts/socket.tsx b/src/contexts/socket.tsx new file mode 100644 index 0000000..abc4c86 --- /dev/null +++ b/src/contexts/socket.tsx @@ -0,0 +1,60 @@ +"use client"; +import { createContext, useContext, useState } from "react"; +import { Socket, io } from "socket.io-client"; + +import { SOCKET_URL } from "@/config"; +type SocketContextType = { + socket: Socket | null; + connectSocket: () => void; + disconnectSocket: () => void; +}; + +export const SocketContext = createContext(null); + +type SocketProviderProps = { + children: React.ReactNode; +}; + +export const SocketProvider = ({ children }: SocketProviderProps) => { + const [socket, setSocket] = useState(null); + + const connectSocket = () => { + if (!socket) { + const newSocket: Socket = io(SOCKET_URL, { + autoConnect: false, + reconnection: true, + reconnectionAttempts: 10, + reconnectionDelay: 3000, + }); + setSocket(newSocket); + return; + } + socket.connect(); + }; + + const disconnectSocket = () => { + if (socket) { + socket.disconnect(); + } + }; + + return ( + + {children} + + ); +}; + +export const useSocket = () => { + const context = useContext(SocketContext); + if (!context) { + throw new Error("useSocket must be used within a SocketProvider"); + } + return context; +}; From 48ca7760f25120c237468d23e79c23327b4fb2af Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Fri, 15 Mar 2024 22:38:05 +0400 Subject: [PATCH 10/21] refactor: order messages by descending --- src/app/api/messages/chat/route.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/api/messages/chat/route.ts b/src/app/api/messages/chat/route.ts index 2a7e9f3..751f92c 100644 --- a/src/app/api/messages/chat/route.ts +++ b/src/app/api/messages/chat/route.ts @@ -27,6 +27,10 @@ export async function GET(request: Request) { where: { conversation_id: conversation_id, }, + + orderBy: { + created_at: "desc", + }, }); const nextId = chat.length < take ? undefined : chat[chat.length - 1].id; From 8a371a09bbf27bdb987a42282c4497e98d17510b Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Fri, 15 Mar 2024 22:39:55 +0400 Subject: [PATCH 11/21] refactor: check if chat exists before reversing it --- src/features/messages/hooks/use-get-chat.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/features/messages/hooks/use-get-chat.ts b/src/features/messages/hooks/use-get-chat.ts index 56b5cd3..3c09eaa 100644 --- a/src/features/messages/hooks/use-get-chat.ts +++ b/src/features/messages/hooks/use-get-chat.ts @@ -19,16 +19,21 @@ export const useChat = (conversation_id: string | undefined) => { return lastPage?.nextId; }, - select: (data) => ({ - pages: [...data.pages].reverse(), - pageParams: [...data.pageParams].reverse(), - }), + select: (data) => { + return { + pages: [...data.pages].reverse(), + pageParams: [...data.pageParams].reverse(), + }; + }, }); const chat = response?.data?.pages?.reduce((acc, page) => { - const reversedChat = [...page.chat].reverse(); + if (page && page.chat) { + const reversedChat = [...page.chat].reverse(); + return [...acc, ...reversedChat]; + } - return [...acc, ...reversedChat]; + return acc; }, [] as IMessage[]); return { From 951a5d8e54d610d23c1edb751938ccfe4de69be3 Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Fri, 15 Mar 2024 22:40:52 +0400 Subject: [PATCH 12/21] chore: move next-aut and react query providers to providers folder --- src/{utils => providers}/next-auth-provider.tsx | 0 src/{utils => providers}/react-query-provider.tsx | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/{utils => providers}/next-auth-provider.tsx (100%) rename src/{utils => providers}/react-query-provider.tsx (100%) diff --git a/src/utils/next-auth-provider.tsx b/src/providers/next-auth-provider.tsx similarity index 100% rename from src/utils/next-auth-provider.tsx rename to src/providers/next-auth-provider.tsx diff --git a/src/utils/react-query-provider.tsx b/src/providers/react-query-provider.tsx similarity index 100% rename from src/utils/react-query-provider.tsx rename to src/providers/react-query-provider.tsx From d0819fcc3e4f505a0d36f203efc0304c7659f1f6 Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Fri, 15 Mar 2024 22:42:00 +0400 Subject: [PATCH 13/21] feat: create AppProviders component and wrap the whole app with it --- src/app/layout.tsx | 59 +++++++++++++++++++---------------------- src/providers/index.tsx | 20 ++++++++++++++ 2 files changed, 48 insertions(+), 31 deletions(-) create mode 100644 src/providers/index.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 50a0b81..753a657 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -9,8 +9,7 @@ import { AuthModalTrigger } 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"; @@ -35,40 +34,38 @@ export default async function RootLayout({ lang="en" > - - -
- -
- -
+ +
+ +
+ +
- + -
- {children} -
+
+ {children} +
-
- - + + + +
+ ); diff --git a/src/providers/index.tsx b/src/providers/index.tsx new file mode 100644 index 0000000..1274fb2 --- /dev/null +++ b/src/providers/index.tsx @@ -0,0 +1,20 @@ +import { FC } from "react"; + +import { SocketProvider } from "@/contexts/socket"; + +import { NextAuthProvider } from "./next-auth-provider"; +import { ReactQueryProvider } from "./react-query-provider"; + +type AppProvidersProps = { + children: React.ReactNode; +}; + +export const AppProviders: FC = ({ children }) => { + return ( + + + {children}; + + + ); +}; From f4ee06ac5edbe3a4abad145a102c05dfde6bad38 Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Mon, 18 Mar 2024 22:57:00 +0400 Subject: [PATCH 14/21] refactor: update use-socket-events hook to work with infinite query --- .../messages/hooks/use-socket-events.ts | 62 +++++++++++++------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/src/features/messages/hooks/use-socket-events.ts b/src/features/messages/hooks/use-socket-events.ts index 486f164..5af440a 100644 --- a/src/features/messages/hooks/use-socket-events.ts +++ b/src/features/messages/hooks/use-socket-events.ts @@ -1,13 +1,11 @@ -import { useQueryClient } from "@tanstack/react-query"; +import { InfiniteData, useQueryClient } from "@tanstack/react-query"; import { useEffect } from "react"; import { socket } from "@/lib/socket-io"; -import { - IMessage, - SocketEmitMessagePayload, - SocketEmitStatusPayload, -} from "../types"; +import { SocketEmitMessagePayload, SocketEmitStatusPayload } from "../types"; + +import { IInfiniteChat } from "./use-get-chat"; export const useSocketEvents = (conversation_id: string | undefined) => { const queryClient = useQueryClient(); @@ -17,17 +15,22 @@ export const useSocketEvents = (conversation_id: string | undefined) => { queryClient.setQueryData( ["chat", conversation_id], - (oldData: IMessage[]) => { - const newData = oldData.map((message: IMessage) => { - if (message.id === data.message_id) { - return { - ...message, - status: data.status, - }; + (oldData: InfiniteData) => { + const newData = oldData?.pages?.map((page) => { + if (page) { + const newChat = page.chat.map((message) => { + if (message.id === data.message_id) { + return { ...message, status: data.status }; + } + + return message; + }); + + return { ...page, chat: newChat }; } - return message; }); - return newData; + + return { ...oldData, pages: newData }; }, ); }); @@ -35,9 +38,32 @@ export const useSocketEvents = (conversation_id: string | undefined) => { socket.on("message", (data: SocketEmitMessagePayload) => { queryClient.setQueryData( ["chat", conversation_id], - (oldData: IMessage[]) => { - const newData = [...oldData, data.message]; - return newData; + (oldData: InfiniteData) => { + const lastPage = oldData.pages?.at(0); + + if (lastPage?.chat && lastPage.chat.length >= 20) { + return { + ...oldData, + pages: [ + { + chat: [data.message], + nextId: data.message.id, + }, + ...oldData.pages, + ], + }; + } else { + return { + ...oldData, + pages: [ + { + chat: [data.message, ...(lastPage?.chat ?? [])], + nextId: lastPage?.nextId, + }, + ...oldData.pages.slice(1), + ], + }; + } }, ); }); From 825ba5985ab51e17639a85ac9605e6b37d2f9170 Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Wed, 20 Mar 2024 15:48:31 +0400 Subject: [PATCH 15/21] chore: get rid of socket context --- src/contexts/socket.tsx | 60 ----------------------------------------- 1 file changed, 60 deletions(-) delete mode 100644 src/contexts/socket.tsx diff --git a/src/contexts/socket.tsx b/src/contexts/socket.tsx deleted file mode 100644 index abc4c86..0000000 --- a/src/contexts/socket.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; -import { createContext, useContext, useState } from "react"; -import { Socket, io } from "socket.io-client"; - -import { SOCKET_URL } from "@/config"; -type SocketContextType = { - socket: Socket | null; - connectSocket: () => void; - disconnectSocket: () => void; -}; - -export const SocketContext = createContext(null); - -type SocketProviderProps = { - children: React.ReactNode; -}; - -export const SocketProvider = ({ children }: SocketProviderProps) => { - const [socket, setSocket] = useState(null); - - const connectSocket = () => { - if (!socket) { - const newSocket: Socket = io(SOCKET_URL, { - autoConnect: false, - reconnection: true, - reconnectionAttempts: 10, - reconnectionDelay: 3000, - }); - setSocket(newSocket); - return; - } - socket.connect(); - }; - - const disconnectSocket = () => { - if (socket) { - socket.disconnect(); - } - }; - - return ( - - {children} - - ); -}; - -export const useSocket = () => { - const context = useContext(SocketContext); - if (!context) { - throw new Error("useSocket must be used within a SocketProvider"); - } - return context; -}; From 1b1f0cb07978b6f3a2bc58a2917dd54329025a95 Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Wed, 20 Mar 2024 15:51:23 +0400 Subject: [PATCH 16/21] chore: update socket io config --- src/features/messages/components/conversation.tsx | 4 ++-- src/lib/socket-io.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/features/messages/components/conversation.tsx b/src/features/messages/components/conversation.tsx index ad743f8..f344347 100644 --- a/src/features/messages/components/conversation.tsx +++ b/src/features/messages/components/conversation.tsx @@ -29,10 +29,10 @@ export const Conversation = () => { useEffect(() => { socket.auth = { conversation_id: id }; - socket.connect(); + socket?.connect(); return () => { - socket.disconnect(); + socket?.disconnect(); }; }, [id]); diff --git a/src/lib/socket-io.ts b/src/lib/socket-io.ts index 50321a9..b1c9097 100644 --- a/src/lib/socket-io.ts +++ b/src/lib/socket-io.ts @@ -6,8 +6,10 @@ const URL = SOCKET_URL; export const socket = io(URL, { autoConnect: false, - reconnectionAttempts: 5, - reconnectionDelay: 5000, + transports: ["websocket"], + reconnection: true, + reconnectionAttempts: 10, + reconnectionDelay: 1000, }); socket.on("connect_error", (error) => { From e627f6e780caacb879856f3629767401e43f53b6 Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Wed, 20 Mar 2024 15:51:44 +0400 Subject: [PATCH 17/21] chore: remove socket context from providers --- src/providers/index.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/providers/index.tsx b/src/providers/index.tsx index 1274fb2..dd2271c 100644 --- a/src/providers/index.tsx +++ b/src/providers/index.tsx @@ -1,7 +1,5 @@ import { FC } from "react"; -import { SocketProvider } from "@/contexts/socket"; - import { NextAuthProvider } from "./next-auth-provider"; import { ReactQueryProvider } from "./react-query-provider"; @@ -12,9 +10,7 @@ type AppProvidersProps = { export const AppProviders: FC = ({ children }) => { return ( - - {children}; - + {children} ); }; From 3e2c77b6f5928ac34ab1d6255908dad035849e11 Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Mon, 25 Mar 2024 22:45:23 +0400 Subject: [PATCH 18/21] fix: target last message correctly, delete console logs --- src/features/messages/components/message-input.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/features/messages/components/message-input.tsx b/src/features/messages/components/message-input.tsx index 777c179..1cd5554 100644 --- a/src/features/messages/components/message-input.tsx +++ b/src/features/messages/components/message-input.tsx @@ -45,7 +45,6 @@ export const MessageInput = ({ img.src = reader.result as string; img.onload = () => { - console.log(); setChosenImage({ url: reader.result, id: Math.random(), @@ -64,8 +63,7 @@ export const MessageInput = ({ conversation_id, ]); - const lastMessage = chat?.pages?.at(-1)?.chat?.at(-1); - + const lastMessage = chat?.pages?.at(0)?.chat?.at(0); if (lastMessage?.sender_id === sender_id) return; socket.emit("status", { status: "seen", message_id: lastMessage?.id }); @@ -130,7 +128,6 @@ export const MessageInput = ({ (oldData: InfiniteData) => { const lastPage = oldData.pages?.at(0); if (lastPage?.chat && lastPage.chat.length >= 20) { - console.log("hello"); return { ...oldData, pages: [ From 4e60fdd4efac449a08b6fa9cb0a2bfc3d625f0c3 Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Tue, 26 Mar 2024 17:28:36 +0400 Subject: [PATCH 19/21] feat: add infinite scroll, scroll to bottom and new message toasts to chat component --- src/features/messages/components/chat.tsx | 146 ++++++++++++++-------- 1 file changed, 91 insertions(+), 55 deletions(-) diff --git a/src/features/messages/components/chat.tsx b/src/features/messages/components/chat.tsx index 6e3a59c..e3054d6 100644 --- a/src/features/messages/components/chat.tsx +++ b/src/features/messages/components/chat.tsx @@ -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"; @@ -15,47 +16,69 @@ 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(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 ; @@ -63,47 +86,60 @@ export const Chat = ({ return (
- {chat?.map((message, index) => { - return ( -
- } + + {data?.pages?.map((page, pageIndex) => { + return page?.chat?.map((message, messageIndex) => { + return ( +
-
- ); + key={message.id} + > + +
+ ); + }); })}
- {!scrolledToBottom && !displayNewMessageToast && ( + + {toast === "new message" && ( )} - {displayNewMessageToast && ( + {toast === "scroll to bottom" && ( )}
); -}; +}); + +Chat.displayName = "Chat"; From db89cedf5e2626f646d332a0b16b4a4db5fc87aa Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Tue, 26 Mar 2024 17:29:22 +0400 Subject: [PATCH 20/21] refactor: reverse data in place and return it --- src/features/messages/hooks/use-get-chat.ts | 26 +++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/features/messages/hooks/use-get-chat.ts b/src/features/messages/hooks/use-get-chat.ts index 3c09eaa..0c9f215 100644 --- a/src/features/messages/hooks/use-get-chat.ts +++ b/src/features/messages/hooks/use-get-chat.ts @@ -20,24 +20,26 @@ export const useChat = (conversation_id: string | undefined) => { }, select: (data) => { + const pages = data?.pages?.map((page) => { + if (page && page.chat) { + return { + ...page, + chat: page.chat.concat().reverse(), + }; + } + + return page; + }); + return { - pages: [...data.pages].reverse(), - pageParams: [...data.pageParams].reverse(), + pages: pages.concat().reverse(), + pageParams: data.pageParams.concat().reverse(), }; }, }); - const chat = response?.data?.pages?.reduce((acc, page) => { - if (page && page.chat) { - const reversedChat = [...page.chat].reverse(); - return [...acc, ...reversedChat]; - } - - return acc; - }, [] as IMessage[]); - return { - data: chat, + data: response.data, isLoading: response.isLoading, isError: response.isError, isFetchingNextPage: response.isFetchingNextPage, From fe672cce569f42b3e727394cecbd27f58630215d Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Tue, 26 Mar 2024 17:30:13 +0400 Subject: [PATCH 21/21] refactor: message component to display status only on the last one --- src/features/messages/components/message.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/messages/components/message.tsx b/src/features/messages/components/message.tsx index e62153d..2765e18 100644 --- a/src/features/messages/components/message.tsx +++ b/src/features/messages/components/message.tsx @@ -11,10 +11,10 @@ import styles from "./styles/message.module.scss"; export const Message = ({ message, - show_status, + show_status = false, }: { message: IMessage; - show_status: boolean; + show_status?: boolean; }) => { const { data: session } = useSession(); const isSender = session?.user?.id === message.sender_id;