4. Study Kasus (Integrasi FE)
Selanjutnya kita akan integraikan socket ynag telah kita dibuat selumnya disii server.
Instalasi Package
Dokumentasi Infinite Scroll disini
Buat Page untuk chat
Stktur Project
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
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;
};
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>;
}
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,
};
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>
</>
);
}
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]);
Integrasi Typing
Instalasi
Penjelasan ....
Membuat Store
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>
</>
);
}
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>