Skip to content

4. Study Kasus (Integrasi FE)

Selanjutnya kita akan integraikan socket ynag telah kita dibuat selumnya disii server.

Instalasi Package

terminal
npm install --save react-infinite-scroll-component

Dokumentasi Infinite Scroll disini

terminal
npm install --save socket.io-client

Buat Page untuk chat

Stktur Project

alt text

Membut interface

admin/chat/interface/index.ts
import { BaseResponseSuccess } from "@/lib/axiosClient";

export interface ChatMessage {
  id?: number;
  sender: {
    id: number;
  };
  receiver: {
    id: number;
  };
  conversation_id: {
    id: number;
  };
  message: string;
  is_read: number;
  file: string;
  room_receiver: string;
  room_sender: string;
  created_at: Date ;
  updated_at: Date;
}

export interface UserChat {
  id?: number;
  created_at?: Date;
  updated_at?: Date;
  user1?: {
    id: number;
    nama: string;
    email: string;
  };
  user2?: {
    id: number;
    nama: string;
    email: string;
  };

  messages: ChatMessage[];
  conversation_id?: number | { id: number };
  latestMessage?: ChatMessage;
  totalMessages : number,
  limit : 0,
  pageSize : 10
}

export interface UserChatList extends BaseResponseSuccess {
  data: UserChat[];
}

export interface Typing {
  sender?: string;
  receiver?: string;
  is_typing?: boolean;
}

Data Dummy

admin/chat/interface/dataDummy.ts
     export const dataDummy = {
  status: "Success",
  message: "OK",
  data: [
    {
      id: 1,
      created_at: "2024-09-08T14:22:00.000Z",
      updated_at: "2024-09-08T14:22:00.000Z",
      user1: {
        id: 3,
        nama: "raihan",
        email: "raihan@gmail.com",
      },
      user2: {
        id: 1,
        nama: "ihsan",
        email: "ihsan@gmail.com",
      },
      messages: [
        {
          id: 5,
          message: "pong\n",
          file: "https:/",
          is_read: 0,
          created_at: "2024-09-10T06:16:26.000Z",
          updated_at: "2024-09-10T06:16:26.000Z",
          sender: {
            id: 1,
          },
          receiver: {
            id: 3,
          },
        },
        {
          id: 4,
          message: "ping",
          file: "https:/",
          is_read: 0,
          created_at: "2024-09-10T06:16:19.000Z",
          updated_at: "2024-09-10T06:16:19.000Z",
          sender: {
            id: 3,
          },
          receiver: {
            id: 1,
          },
        },
        {
          id: 3,
          message: "ok",
          file: "https:/",
          is_read: 0,
          created_at: "2024-09-10T06:16:11.000Z",
          updated_at: "2024-09-10T06:16:11.000Z",
          sender: {
            id: 3,
          },
          receiver: {
            id: 1,
          },
        },
        {
          id: 2,
          message: "hello",
          file: "https:/",
          is_read: 0,
          created_at: "2024-09-10T06:16:05.000Z",
          updated_at: "2024-09-10T06:16:05.000Z",
          sender: {
            id: 1,
          },
          receiver: {
            id: 3,
          },
        },
        {
          id: 1,
          message: "tes",
          file: "https:/",
          is_read: 0,
          created_at: "2024-09-10T06:15:57.000Z",
          updated_at: "2024-09-10T06:15:57.000Z",
          sender: {
            id: 1,
          },
          receiver: {
            id: 3,
          },
        },
      ],
      conversation_id: 1,
      latestMessage: {
        id: 5,
        message: "pong\n",
        file: "https:/",
        is_read: 0,
        created_at: "2024-09-10T06:16:26.000Z",
        updated_at: "2024-09-10T06:16:26.000Z",
        sender: {
          id: 1,
        },
        receiver: {
          id: 3,
        },
      },
    },
    {
      id: 2,
      created_at: "2024-09-08T14:23:57.000Z",
      updated_at: "2024-09-08T14:23:57.000Z",
      user1: {
        id: 2,
        nama: "aziz",
        email: "aziz@gmail.com",
      },
      user2: {
        id: 1,
        nama: "ihsan",
        email: "ihsan@gmail.com",
      },
      messages: [],
      conversation_id: 2,
      latestMessage: null,
    },
    {
      id: 3,
      created_at: "2024-09-08T14:24:25.000Z",
      updated_at: "2024-09-08T14:24:25.000Z",
      user1: {
        id: 1,
        nama: "ihsan",
        email: "ihsan@gmail.com",
      },
      user2: {
        id: 4,
        nama: "ariq",
        email: "ariq@gmail.com",
      },
      messages: [],
      conversation_id: 3,
      latestMessage: null,
    },
  ],
};

Membuat hook useMessage

admin/chat/socket/useMessage
import { ChangeEvent, useState } from "react";

const useMessage = ()=> {
    const [message, setMessage] = useState <string>("");

    const handleMessage = (e: ChangeEvent<any>) => {
        setMessage(e.target.value)
    }
   return {message, handleMessage, setMessage}
}


export default useMessage
admin/chat/page.tsx
"use client";
import { useEffect, useState } from "react";
import useConversation from "./lib";
import clsx from "clsx";
import { useSession } from "next-auth/react";
import useWebSocket from "@/app/admin/chat/socket/useWebSocket";
import useMessage from "@/app/admin/chat/socket/useMessage";
import InfiniteScroll from "react-infinite-scroll-component";
import { ChatMessage, UserChat, UserChatList } from "./interface";
import { dataDummy as data } from "./interface/dummy";

export default function Chat() {
  const { data: session } = useSession();
  const [chatList, setChatList] = useState<ChatMessage[]>([]);
  const { message, handleMessage } = useMessage();
   const [selected, setSelected] = useState<UserChat>({
    totalMessages: 0,
    messages: [],
    page :1,
    limit : 10
  });



  return (
    <>
      <div className="grid grid-cols-7 h-screen w-full bg-[#0C141A]  ">
        <section className="col-span-2 h-scren pt-10 bg-[#111B21]">
          {data &&
            data.data.map((item: any, index: number) => {
              return (
                <section
                  key={index}
                  onClick={() => {
                    setSelected(item);
                    console.log("item", item);
                    setChatList(() => {
                      return item.messages;
                    });
                  }}
                  className={clsx(
                    `flex items-center space-x-2 mb-4 cursor-pointer   p-2`,
                    {
                      "bg-[#2A3942]":
                        selected.conversation_id === item.conversation_id,
                    }
                  )}
                >
                  <div className="bg-red-400 h-10 w-10 rounded-full"></div>
                  <div className="text-white flex flex-col">
                    <span>
                      {item.user1.email !== session?.user.email
                        ? item.user1.nama
                        : item.user2.nama}
                    </span>
                    <span className="text-xs text-[#6E7D87]">
                      {/* {item.latestMessage?.message} */}
                    </span>
                  </div>
                </section>
              );
            })}
        </section>
        <section className="col-span-5 h-screen">
          <div className="h-[60px] w-full bg-[#1F2C33] flex space-x-2 items-center px-3  ">
            <div className="bg-red-400 h-10 w-10 rounded-full"></div>
            <div className="text-white flex flex-col">
              <span>
                {selected?.user1?.email !== session?.user.email
                  ? selected?.user1?.nama
                  : selected?.user2?.nama}
              </span>
              <span className="text-xs text-[#6E7D87]">
              online
              </span>
            </div>
          </div>
          <div
            id="ticketLists"
            className="h-screen-160 text-white flex flex-col-reverse overflow-auto px-5"
          >
            <InfiniteScroll
              style={{ overflowX: "hidden", overflowY: "hidden" }}
              dataLength={chatList.length} //This is important field to render the next data
              next={() => {}}
              className="flex flex-col-reverse pb-10 space-y-5"
            hasMore={selected.totalMessages > selected.messages?.length}
              loader={<h4>Loading...</h4>}
              endMessage={
                <p style={{ textAlign: "center" }}>
                  <b>Yay! You have seen it all</b>
                </p>
              }
              inverse={true}
              scrollableTarget="ticketLists"
            >
              {[...chatList].map((x: ChatMessage, i: number) => (
                <div
                  className={clsx(
                    "w-full flex mt-5", // Memastikan pesan menggunakan lebar penuh
                    {
                      "justify-end": x.sender.id === session?.user.id, // Pesan di sebelah kanan
                      "justify-start": x.sender.id !== session?.user.id, // Pesan di sebelah kiri
                    }
                  )}
                  key={i}
                >
                  <div
                    className={clsx(
                      "px-1 py-1 rounded max-w-[75%] break-words", // Membatasi lebar pesan max 75% dan membiarkan teks membungkus
                      {
                        "bg-[#015C4B]": x.sender.id === session?.user.id, // Warna untuk pesan user sendiri
                        "bg-[#1D282F]": x.sender.id !== session?.user.id, // Warna untuk pesan orang lain
                      }
                    )}
                  >
                    <span className="text-sm whitespace-pre-wrap">
                      {x.message}
                    </span>
                    <span className="text-[10px] text-right block">
                      {formatDate(x.created_at)}
                    </span>
                  </div>
                </div>
              ))}
            </InfiniteScroll>
          </div>
          <div className="h-[100px]  bg-[#1F2C33] w-full p-5 flex items-center space-x-5 ">
            <div className="w-[80%]">
              <textarea
                placeholder="ketik pesan"
                value={message}
                onChange={handleMessage}
                className="bg-gray-200 w-full p-3 rounded-md"
              />
            </div>

            <div className="w-[20%] space-x-5">
              <button
                disabled={message.length === 0}
                className="text-white bg-[#04A784] h-[40px] rounded-lg w-full"
              >
                Kirim
              </button>
            </div>
          </div>
        </section>
      </div>
    </>
  );
}

const formatDate = (timestamp: Date) => {
  const date = new Date(timestamp);


  const formattedDate = date.toLocaleString("id-ID", {
    day: "2-digit",
    month: "2-digit",
    year: "numeric",
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
  });

  return formattedDate;
};

Hasil

alt text

Integrasikan APi list

admin/chat/lib/index.ts
import { useSession } from "next-auth/react";
import useAxiosAuth from "@/hook/useAxiosAuth";
import { useMutation, useQuery } from "@tanstack/react-query";

const useConversation = () => {
  const axiosAuthClient = useAxiosAuth();
  const { data: session } = useSession();

  const getList = () => {
    return axiosAuthClient.get("/chat/list").then((res) => res.data);
  };

  const useGetList = () => {
    const { data, isFetching, isLoading, isError } = useQuery(
      ["/chat/list"],
      () => getList(),
      {
        keepPreviousData: true,
        enabled: !!session === true,
        staleTime: 6000 * 60 * 60 * 24, // 1 hari

        select: (response) => response,
      }
    );

    return {
      data,
      isFetching,
    };
  };





  return { useGetList };
};

export default useConversation;
chat/page.tsx
"use client";
import { useEffect, useState } from "react";
import useConversation from "./lib";
import clsx from "clsx";
import { useSession } from "next-auth/react";
import useWebSocket from "@/app/admin/chat/socket/useWebSocket";
import useMessage from "@/app/admin/chat/socket/useMessage";
import InfiniteScroll from "react-infinite-scroll-component";
import { ChatMessage, UserChat, UserChatList } from "./interface";


export default function Chat() {
  const { data: session } = useSession();
  const {useGetList}= useConversation()
  const {data, isFetching} = useGetList()
  const [chatList, setChatList] = useState<ChatMessage[]>([]);
  const { message, handleMessage } = useMessage();
   const [selected, setSelected] = useState<UserChat>({
    totalMessages: 0,
    messages: [],
    limit :0,
    pageSize : 10
  });

  if(isFetching){
    return <div>Mengambil data chat ...</div>
  }

  return (
    <>
      <div className="grid grid-cols-7 h-screen w-full bg-[#0C141A]  ">
        <section className="col-span-2 h-scren pt-10 bg-[#111B21]">
          {data &&
            data.data.map((item: any, index: number) => {
              return (
                <section
                  key={index}
                  onClick={() => {
                    setSelected(item);
                    console.log("item", item);
                    setChatList(() => {
                      return item.messages;
                    });
                  }}
                  className={clsx(
                    `flex items-center space-x-2 mb-4 cursor-pointer   p-2`,
                    {
                      "bg-[#2A3942]":
                        selected.conversation_id === item.conversation_id,
                    }
                  )}
                >
                  <div className="bg-red-400 h-10 w-10 rounded-full"></div>
                  <div className="text-white flex flex-col">
                    <span>
                      {item.user1.email !== session?.user.email
                        ? item.user1.nama
                        : item.user2.nama}
                    </span>
                    <span className="text-xs text-[#6E7D87]">
                      {/* {item.latestMessage?.message} */}
                    </span>
                  </div>
                </section>
              );
            })}
        </section>
        <section className="col-span-5 h-screen">
          <div className="h-[60px] w-full bg-[#1F2C33] flex space-x-2 items-center px-3  ">
            <div className="bg-red-400 h-10 w-10 rounded-full"></div>
            <div className="text-white flex flex-col">
              <span>
                {selected?.user1?.email !== session?.user.email
                  ? selected?.user1?.nama
                  : selected?.user2?.nama}
              </span>
              <span className="text-xs text-[#6E7D87]">
              online
              </span>
            </div>
          </div>
          <div
            id="ticketLists"
            className="h-screen-160 text-white flex flex-col-reverse overflow-auto px-5"
          >
            <InfiniteScroll
              style={{ overflowX: "hidden", overflowY: "hidden" }}
              dataLength={chatList.length} //This is important field to render the next data
              next={() => {}}
              className="flex flex-col-reverse pb-10 space-y-5"
              hasMore={selected.totalMessages > selected.messages?.length}
              loader={<h4>Loading...</h4>}
              endMessage={
                <p style={{ textAlign: "center" }}>
                  <b>Yay! You have seen it all</b>
                </p>
              }
              inverse={true}
              scrollableTarget="ticketLists"
            >
              {[...chatList].map((x: ChatMessage, i: number) => (
                <div
                  className={clsx(
                    "w-full flex mt-5", // Memastikan pesan menggunakan lebar penuh
                    {
                      "justify-end": x.sender.id === session?.user.id, // Pesan di sebelah kanan
                      "justify-start": x.sender.id !== session?.user.id, // Pesan di sebelah kiri
                    }
                  )}
                  key={i}
                >
                  <div
                    className={clsx(
                      "px-1 py-1 rounded max-w-[75%] break-words", // Membatasi lebar pesan max 75% dan membiarkan teks membungkus
                      {
                        "bg-[#015C4B]": x.sender.id === session?.user.id, // Warna untuk pesan user sendiri
                        "bg-[#1D282F]": x.sender.id !== session?.user.id, // Warna untuk pesan orang lain
                      }
                    )}
                  >
                    <span className="text-sm whitespace-pre-wrap">
                      {x.message}
                    </span>
                    <span className="text-[10px] text-right block">
                      {formatDate(x.created_at)}
                    </span>
                  </div>
                </div>
              ))}
            </InfiniteScroll>
          </div>
          <div className="h-[100px]  bg-[#1F2C33] w-full p-5 flex items-center space-x-5 ">
            <div className="w-[80%]">
              <textarea
                placeholder="ketik pesan"
                value={message}
                onChange={handleMessage}
                className="bg-gray-200 w-full p-3 rounded-md"
              />
            </div>

            <div className="w-[20%] space-x-5">
              <button
                disabled={message.length === 0}
                className="text-white bg-[#04A784] h-[40px] rounded-lg w-full"
              >
                Kirim
              </button>
            </div>
          </div>
        </section>
      </div>
    </>
  );
}

const formatDate = (timestamp: Date) => {
  const date = new Date(timestamp);

  // Format menjadi tanggal dan jam dalam bahasa Indonesia
  const formattedDate = date.toLocaleString("id-ID", {
    day: "2-digit",
    month: "2-digit",
    year: "numeric",
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
  });

  return formattedDate;
};

alt text alt text

Menghubungkan ke socket

.env
NEXTAUTH_URL=http://localhost:3010
NEXTAUTH_SECRET=dankanfklgnakgnakn


NEXT_PUBLIC_API_URL=http://localhost:5002
NEXT_PUBLIC_SOCKET_URL=http://localhost:5002


NEXT_PUBLIC_WS_URL=http://localhost:5002/events
chat/socket/useWebsocket.ts
import { useSession } from "next-auth/react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { io } from "socket.io-client";
import { useQueryClient } from "@tanstack/react-query";

const useWebSocket = () => {
  const { data: session } = useSession();
  const queryClient = useQueryClient();

  // Menggunakan useMemo untuk menginisialisasi socket hanya sekali
  const socket = useMemo(
    () =>
      io(`${process.env.NEXT_PUBLIC_API_URL}`, {
        autoConnect: true,
        transports: ["websocket"],
        // query: { token: session?.user?.accessToken },
      }),
    [] // socket hanya diinisialisasi sekali
  );

  useEffect(() => {
    // Event listener untuk connect dan disconnect
    const onConnect = () => {
      console.log("socket is connected");
    };

    const onDisconnect = () => {
      console.log("socket is disconnected");
    };

    socket.on("connect", onConnect);
    socket.on("disconnect", onDisconnect);

    return () => {
      socket.off("connect", onConnect);
      socket.off("disconnect", onDisconnect);
    };
  }, [socket]);

  return {
    socket,
  };
};

export default useWebSocket;

Jalankan WebSocket di layout pada fitur Admin

admin/layout.tsx
'use client'
import useWebSocket from "./chat/socket/useWebSocket";
import { ReactNode } from "react";

interface AdminLayoutProps {
  children: ReactNode;
}


export default function AdminLayout({ children }: AdminLayoutProps) {

  const {socket} = useWebSocket()

  console.log('socket', socket)



  return <div className="bg-blue-2000">{children}</div>;
}

alt text

Membuat Event join room agar akun siap menerima pesan

chat/socket/useWebsocket.ts
 useEffect(() => {

    const onConnect = () => {
      console.log("socket is connected");
    };

    const onDisconnect = () => {
      console.log("socket is disconnected");
    };

     const onJoinReply = (data: any) => {
      console.log("join.reply", data);
    };

    socket.on("connect", onConnect);
    socket.on("disconnect", onDisconnect);
    socket.on("join.reply", onJoinReply);

    return () => {
      socket.off("connect", onConnect);
      socket.off("disconnect", onDisconnect);
      socket.off("join.reply", onJoinReply);
    };
  }, [socket]);


  useEffect(() => {
    if (session?.user?.email) {
      socket.emit("join", { room_code: session?.user?.email });
    }
  }, [session]);

  return {
    socket,
  };

alt text

Integrasi api untuk mengirimkan pesan

a

chat/lib/index.ts
import { useSession } from "next-auth/react";
import useAxiosAuth from "@/hook/useAxiosAuth";
import { useMutation, useQuery } from "@tanstack/react-query";

const useConversation = () => {
  const axiosAuthClient = useAxiosAuth();
  const { data: session } = useSession();

 ....

  const useSendMessage = () => {
    const mutate = useMutation((payload: any) => {
      return axiosAuthClient.post("/chat/send_message", payload);
    });
    return mutate;
  };

  return { useGetList, useSendMessage };
};

export default useConversation;

v

chat/socket/useWebsocket.ts
useEffect(() => {

    const onConnect = () => {
      console.log("socket is connected");
    };

    const onDisconnect = () => {
      console.log("socket is disconnected");
    };

     const onJoinReply = (data: any) => {
      console.log("join.reply", data);
    };
     const onReceivedMessage = (data: any) => {
      console.log("received_message", data);

    };

    socket.on("connect", onConnect);
    socket.on("disconnect", onDisconnect);
    socket.on("join.reply", onJoinReply);
    socket.on("received_message", onReceivedMessage);

    return () => {
      socket.off("connect", onConnect);
      socket.off("disconnect", onDisconnect);
      socket.off("join.reply", onJoinReply);
      socket.off("received_message", onReceivedMessage);
    };
  }, [socket]);
chat/page.tsx
"use client";
import { useEffect, useState } from "react";
import useConversation from "./lib";
import clsx from "clsx";
import { useSession } from "next-auth/react";
import useWebSocket from "@/app/admin/chat/socket/useWebSocket";
import useMessage from "@/app/admin/chat/socket/useMessage";
import InfiniteScroll from "react-infinite-scroll-component";
import { ChatMessage, UserChat, UserChatList } from "./interface";


export default function Chat() {
  const { data: session } = useSession();
  const {useGetList, useSendMessage}= useConversation()
  const {data, isFetching} = useGetList()
  const [chatList, setChatList] = useState<ChatMessage[]>([]);
  const { message, handleMessage , setMessage} = useMessage();
   const [selected, setSelected] = useState<UserChat>({
    totalMessages: 0,
    messages: [],
    limit :0,
    pageSize : 10
  });
  const mutateSendMessage = useSendMessage();

  if(isFetching){
    return <div>Mengambil data chat ...</div>
  }

  return (
    <>
      <div className="grid grid-cols-7 h-screen w-full bg-[#0C141A]  ">
        <section className="col-span-2 h-scren pt-10 bg-[#111B21]">
          {data &&
            data.data.map((item: any, index: number) => {
              return (
                <section
                  key={index}
                  onClick={() => {
                    setSelected(item);
                    console.log("item", item);
                    setChatList(() => {
                      return item.messages;
                    });
                  }}
                  className={clsx(
                    `flex items-center space-x-2 mb-4 cursor-pointer   p-2`,
                    {
                      "bg-[#2A3942]":
                        selected.conversation_id === item.conversation_id,
                    }
                  )}
                >
                  <div className="bg-red-400 h-10 w-10 rounded-full"></div>
                  <div className="text-white flex flex-col">
                    <span>
                      {item.user1.email !== session?.user.email
                        ? item.user1.nama
                        : item.user2.nama}
                    </span>
                    <span className="text-xs text-[#6E7D87]">
                      {/* {item.latestMessage?.message} */}
                    </span>
                  </div>
                </section>
              );
            })}
        </section>
        <section className="col-span-5 h-screen">
          <div className="h-[60px] w-full bg-[#1F2C33] flex space-x-2 items-center px-3  ">
            <div className="bg-red-400 h-10 w-10 rounded-full"></div>
            <div className="text-white flex flex-col">
              <span>
                {selected?.user1?.email !== session?.user.email
                  ? selected?.user1?.nama
                  : selected?.user2?.nama}
              </span>
              <span className="text-xs text-[#6E7D87]">
              online
              </span>
            </div>
          </div>
          <div
            id="ticketLists"
            className="h-screen-160 text-white flex flex-col-reverse overflow-auto px-5"
          >
            <InfiniteScroll
              style={{ overflowX: "hidden", overflowY: "hidden" }}
              dataLength={chatList.length} //This is important field to render the next data
              next={() => {}}
              className="flex flex-col-reverse pb-10 space-y-5"
            hasMore={selected.totalMessages > selected.messages?.length}
              loader={<h4>Loading...</h4>}
              endMessage={
                <p style={{ textAlign: "center" }}>
                  <b>Yay! You have seen it all</b>
                </p>
              }
              inverse={true}
              scrollableTarget="ticketLists"
            >
              {[...chatList].map((x: ChatMessage, i: number) => (
                <div
                  className={clsx(
                    "w-full flex mt-5", // Memastikan pesan menggunakan lebar penuh
                    {
                      "justify-end": x.sender.id === session?.user.id, // Pesan di sebelah kanan
                      "justify-start": x.sender.id !== session?.user.id, // Pesan di sebelah kiri
                    }
                  )}
                  key={i}
                >
                  <div
                    className={clsx(
                      "px-1 py-1 rounded max-w-[75%] break-words", // Membatasi lebar pesan max 75% dan membiarkan teks membungkus
                      {
                        "bg-[#015C4B]": x.sender.id === session?.user.id, // Warna untuk pesan user sendiri
                        "bg-[#1D282F]": x.sender.id !== session?.user.id, // Warna untuk pesan orang lain
                      }
                    )}
                  >
                    <span className="text-sm whitespace-pre-wrap">
                      {x.message}
                    </span>
                    <span className="text-[10px] text-right block">
                      {formatDate(x.created_at)}
                    </span>
                  </div>
                </div>
              ))}
            </InfiniteScroll>
          </div>
          <div className="h-[100px]  bg-[#1F2C33] w-full p-5 flex items-center space-x-5 ">
            <div className="w-[80%]">
              <textarea
                placeholder="ketik pesan"
                value={message}
                onChange={handleMessage}
                className="bg-gray-200 w-full p-3 rounded-md"
              />
            </div>

            <div className="w-[20%] space-x-5">
              <button
                disabled={message.length === 0}
                className="text-white bg-[#04A784] h-[40px] rounded-lg w-full"
                onClick={() => {
                  mutateSendMessage.mutate(
                    {
                      message: message,
                      room_receiver:
                        selected?.user1?.email !== session?.user.email
                          ? selected?.user1?.email
                          : selected?.user2?.email,

                      conversation_id: {
                        id: selected?.id,
                      },
                      file: "https:/",
                      receiver: {
                        id:
                          selected?.user1?.email !== session?.user.email
                            ? selected?.user1?.id
                            : selected?.user2?.id,
                      },
                    },
                    {
                      onSuccess: () => {
                        setMessage("");
                      },
                    }
                  );
                }}
              >
                Kirim
              </button>
            </div>
          </div>
        </section>
      </div>
    </>
  );
}

alt text alt text

kita telah behasil ...

Modifiksi cache react query

untuk ...

chat/socket/useCache
import { useQueryClient } from "@tanstack/react-query";
import { ChatMessage, UserChat, UserChatList } from "../interface";

const useCache = () => {
  const queryClient = useQueryClient();
  const handleNewMessage = async (data: ChatMessage) => {
    await queryClient.cancelQueries(["/chat/list"]);



    // Ambil data sebelumnya dari cache
    const previousConversations = queryClient.getQueryData<UserChatList>([
      "/chat/list",
    ]);

    if (previousConversations) {
      // Temukan apakah conversation_id sudah ada
      const existingConversationIndex = previousConversations.data.findIndex(
        (convo: UserChat) => convo.conversation_id === data.conversation_id.id
      );

      if (existingConversationIndex !== -1) {
        // Update existing conversation
        const updatedConversations = previousConversations.data.map((convo) => {
          if (convo.conversation_id === data.conversation_id.id) {
            // Tambahkan pesan baru ke array messages
            const latest = convo.messages || [];
            const updatedMessages = [data, ...latest];
            return {
              ...convo,
              messages: updatedMessages,
              latestMessage: data, // Update latestMessage untuk percakapan ini
            };
          }
          return convo;
        });

        console.log("updatedConversations", updatedConversations);
        const sortedConversations = updatedConversations.sort((a, b) => {
          const latestMessageA = a.latestMessage?.created_at
            ? new Date(a.latestMessage.created_at)
            : new Date(0);
          const latestMessageB = b.latestMessage?.created_at
            ? new Date(b.latestMessage.created_at)
            : new Date(0);
          return latestMessageB.getTime() - latestMessageA.getTime(); // Urutkan dari yang terbaru
        });

        // Perbarui cache dengan data yang telah diperbarui
        queryClient.setQueryData(["/chat/list"], {
          ...previousConversations,
          data: sortedConversations,
        });
      } else {
        // Jika conversation_id baru, tambahkan ke data
        const updatedData = [
          {
            id: data.conversation_id.id,
            created_at: new Date().toISOString(),
            updated_at: new Date().toISOString(),
            user1: {}, // Sesuaikan dengan data yang ada
            user2: {}, // Sesuaikan dengan data yang ada
            messages: [data],
            latestMessage: data,
            conversation_id: data.conversation_id.id,
          },
          ...previousConversations.data,
        ];

        const sortedConversations = updatedData.sort((a, b) => {
          const latestMessageA = a.latestMessage?.created_at
            ? new Date(a.latestMessage.created_at)
            : new Date(0);
          const latestMessageB = b.latestMessage?.created_at
            ? new Date(b.latestMessage.created_at)
            : new Date(0);
          return latestMessageB.getTime() - latestMessageA.getTime(); // Urutkan dari yang terbaru
        });

        // Perbarui cache dengan data yang telah diperbarui
        queryClient.setQueryData(["/chat/list"], {
          ...previousConversations,
          data: sortedConversations,
        });
      }
    } else {
      // Jika tidak ada data sebelumnya, set data baru ke cache
      queryClient.setQueryData(["/chat/list"], {
        status: "Success",
        message: "OK",
        data: [
          {
            id: data.conversation_id.id,
            created_at: new Date().toISOString(),
            updated_at: new Date().toISOString(),
            user1: {}, // Sesuaikan dengan data yang ada
            user2: {}, // Sesuaikan dengan data yang ada
            messages: [data],
            latestMessage: data,
            conversation_id: data.conversation_id.id,
          },
        ],
      });
    }
  };

  return {handleNewMessage}
};


export default useCache
chat/socket/useWebsocket
...
import useCache from "./useCache";

const useWebSocket = () => {
  const { data: session } = useSession();
  const {handleNewMessage}= useCache()

 ....

  useEffect(() => {

    ...

     const onReceivedMessage = (data: any) => {
      console.log("received_message", data);
      handleNewMessage(data);
    };

    socket.on("connect", onConnect);
    socket.on("disconnect", onDisconnect);
    socket.on("join.reply", onJoinReply);
    socket.on("received_message", onReceivedMessage);

    return () => {
      socket.off("connect", onConnect);
      socket.off("disconnect", onDisconnect);
      socket.off("join.reply", onJoinReply);
      socket.off("received_message", onReceivedMessage);
    };
  }, [socket]);

....

};

export default useWebSocket;
chat/page.tsx
  useEffect(() => {
    if (!!selected === true) {
      let filtterd = data?.data?.filter(
        (x: UserChat) => x.conversation_id === selected?.conversation_id
      );
      setChatList(filtterd?.[0]?.messages || []);
    }
  }, [data]);

alt text alt text

Integrasi Typing

Instalasi

terminal
npm install zustand --save

Penjelasan ....

Membuat Store

alt text

store/useStoreChat
import { Typing } from "@/app/admin/chat/interface";
import { create } from "zustand";
import { Socket } from "socket.io-client";

interface ChatState {
  socket: Socket | null;
  setSocket: (newSocket: Socket) => void;
  typing: Typing;
  setTyping: (newTyping: Typing) => void;
}

const useStoreChat = create<ChatState>((set) => ({
  socket: null,
  setSocket: (newSocket: Socket) => {
    set((state) => ({
      ...state,
      socket: newSocket,
    }));
  },
  typing: {} as Typing,
  setTyping: (newTyping: Typing) => {
    set((state) => ({
      ...state,
      typing: newTyping,
    }));
  },
}));

export default useStoreChat;

Meyimpan socket ke dalam store

admin/chat/layout.ts
"use client";
import useStoreChat from "@/store/useStoreChat";
import useWebSocket from "./chat/socket/useWebSocket";
import { useEffect, ReactNode } from "react";

interface AdminLayoutProps {
  children: ReactNode; 
}

export default function AdminLayout({ children }: AdminLayoutProps) {
  const { socket } = useWebSocket();
  const setSocket = useStoreChat((state) => state.setSocket);

  useEffect(() => {
    setSocket(socket);
  }, [socket]);

  return <div className="bg-blue-2000">{children}</div>;
}

Membuat function ketika typing

chat/socket/useEmitSocket.ts
import { useSession } from "next-auth/react";
import useStoreChat from "@/store/useStoreChat";

const useEmitSocket = () => {
  const { data: session } = useSession();
  const socket = useStoreChat((state:any)=> state.socket)

  const typingHandle = (receiver: string, is_typing: boolean): void => {
    socket.emit("typing", {
      sender: session?.user?.email,
      receiver: receiver,
      is_typing: is_typing,
    });
  };


  return {
    typingHandle,

  };
};

export default useEmitSocket;
chat/socket/useWebSocket.ts
import { useSession } from "next-auth/react";
import { useEffect, useMemo } from "react";
import { io } from "socket.io-client";
import useCache from "./useCache";
import useStoreChat from "@/store/useStoreChat";

const useWebSocket = () => {
  const { data: session } = useSession();
  const { handleNewMessage } = useCache();
  const setTyping = useStoreChat((state: any) => state.setTyping);

 ...

  useEffect(() => {

   ...

    const onTypingListen = (data: any) => {
      console.log("typing.listen", data);
      setTyping(data);
    };


...

    socket.on("connect", onConnect);
    socket.on("disconnect", onDisconnect);
    socket.on("join.reply", onJoinReply);
    socket.on("received_message", onReceivedMessage);
     socket.on("typing.listen", onTypingListen);


    return () => {
      socket.off("connect", onConnect);
      socket.off("disconnect", onDisconnect);
      socket.off("join.reply", onJoinReply);
      socket.off("received_message", onReceivedMessage);
      socket.off("typing.listen", onTypingListen);
    };
  }, [socket]); 

 ...

  return {
    socket,
  };
};

export default useWebSocket;
chat/page.tsx
"use client";
import { useEffect, useState } from "react";
import useConversation from "./lib";
import clsx from "clsx";
import { useSession } from "next-auth/react";
import useWebSocket from "@/app/admin/chat/socket/useWebSocket";
import useMessage from "@/app/admin/chat/socket/useMessage";
import InfiniteScroll from "react-infinite-scroll-component";
import { ChatMessage, UserChat, UserChatList } from "./interface";
import useEmitSocket from "./socket/useEmitSocket";
import useStoreChat from "@/store/useStoreChat";

export default function Chat() {
 ... 

  const { typingHandle } = useEmitSocket()
  const typing = useStoreChat((state:any)=> state.typing)


  if (isFetching) {
    return <div>Mengambil data chat ...</div>;
  }

  return (
    <>
      <div className="grid grid-cols-7 h-screen w-full bg-[#0C141A]  ">

...

        <section className="col-span-5 h-screen">
          <div className="h-[60px] w-full bg-[#1F2C33] flex space-x-2 items-center px-3  ">
            <div className="bg-red-400 h-10 w-10 rounded-full"></div>
            <div className="text-white flex flex-col">
              <span>
                {selected?.user1?.email !== session?.user.email
                  ? selected?.user1?.nama
                  : selected?.user2?.nama}
              </span>
             <span className="text-xs text-[#6E7D87]">
                {typing.is_typing === true && typing.sender === (selected?.user1?.email !== session?.user.email
                  ? selected?.user1?.email
                  : selected?.user2?.email) ? "sedang mengetik" : "online"}
              </span>
            </div>
          </div>
          <div
            id="ticketLists"
            className="h-screen-160 text-white flex flex-col-reverse overflow-auto px-5"
          >

          </div>
          <div className="h-[100px]  bg-[#1F2C33] w-full p-5 flex items-center space-x-5 ">
            <div className="w-[80%]">
              <textarea
              onBlur={() => {
                typingHandle(
                  selected?.user1?.email !== session?.user.email
                    ? `${selected?.user1?.email}`
                    : `${selected?.user2?.email}`, false
                );
              }}
                onFocus={() => {
                  typingHandle(
                    selected?.user1?.email !== session?.user.email
                      ? `${selected?.user1?.email}`
                      : `${selected?.user2?.email}`, true
                  );
                }}
                placeholder="ketik pesan"
                value={message}
                onChange={handleMessage}
                className="bg-gray-200 w-full p-3 rounded-md"
              />
            </div>


          </div>
        </section>
      </div>
    </>
  );
}

alt text

ubah layout

chat/page.tsx
 <section className="col-span-2 h-scren  bg-[#111B21]">
          <div className="h-[60px] bg-[#1F2C33] px-5 w-full flex items-center justify-between">
            <h5 className="text-white">{session?.user?.name}</h5>
            <button className="text-white p-1 border rounded-md">+</button>
          </div>
          <div className="h-screen-60 overflow-auto">
            {data &&
              data.data.map((item: any, index: number) => {
                return (
                  <section
                    key={index}
                    onClick={() => {
                      setSelected(item);
                      console.log("item", item);
                      setChatList(() => {
                        return item.messages;
                      });
                    }}
                    className={clsx(
                      `flex items-center space-x-2 mb-4 cursor-pointer   p-2`,
                      {
                        "bg-[#2A3942]":
                          selected.conversation_id === item.conversation_id,
                      }
                    )}
                  >
                    <div className="bg-red-400 h-10 w-10 rounded-full"></div>
                    <div className="text-white flex flex-col">
                      <span>
                        {item.user1.email !== session?.user.email
                          ? item.user1.nama
                          : item.user2.nama}
                      </span>
                      <span className="text-xs text-[#6E7D87]">
                        {/* {item.latestMessage?.message} */}
                      </span>
                    </div>
                  </section>
                );
              })}
          </div>
        </section>

alt text