339 lines
11 KiB
TypeScript
339 lines
11 KiB
TypeScript
// src/hooks/useChatSocket.js
|
|
|
|
import { useApi } from '@/api';
|
|
import { IMessage } from '@/api/resources/ChatMessages';
|
|
// import { useAuth } from '@/auth';
|
|
import routes from '@/routes';
|
|
// import { getZodiacSignByDate } from '@/services/zodiac-sign';
|
|
import { actions, selectors } from '@/store';
|
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import { io, Socket } from 'socket.io-client';
|
|
import { ClientToServerEvents, IBalanceUpdated, ICurrentBalance, IResponse, ISessionStarted, ServerToClientEvents } from './data';
|
|
import { sleep } from '@/services/date';
|
|
|
|
const compareDates = (date1: string, date2: string) => {
|
|
return new Date(date1).getTime() - new Date(date2).getTime();
|
|
}
|
|
|
|
const isAvailableDateUsing = (dateEnd?: string) => {
|
|
if (!dateEnd) {
|
|
return false
|
|
}
|
|
return compareDates(dateEnd, new Date().toString()) > 0
|
|
// return (new Date(dateEnd).getTime() - new Date().getTime()) > 0
|
|
}
|
|
|
|
|
|
const useChatSocket = (userId: string, chatId: string) => {
|
|
const dispatch = useDispatch();
|
|
const token = useSelector(selectors.selectToken)
|
|
const api = useApi();
|
|
const [socket, setSocket] = useState<Socket<ServerToClientEvents, ClientToServerEvents> | null>(null);
|
|
const [messages, setMessages] = useState<IMessage[]>([]);
|
|
const [balance, setBalance] = useState<ICurrentBalance>();
|
|
const [session, setSession] = useState<ISessionStarted | null>();
|
|
// const [dateEnded, setDateEnded] = useState<string>("");
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
// For first modal shown
|
|
const [initialBalance, setInitialBalance] = useState<number | null>(null);
|
|
|
|
const [isLoadingSelfMessage, setIsLoadingSelfMessage] = useState(false);
|
|
const [isLoadingAdvisorMessage, setIsLoadingAdvisorMessage] = useState(false);
|
|
const [isAvailableChatting, setIsAvailableChatting] = useState(true);
|
|
|
|
const dateEndChattingFromStore = useSelector(selectors.selectDateEndChatting);
|
|
|
|
const dateEndChatting = useMemo(() => {
|
|
// return "2024-11-17T00:26:19.329Z"
|
|
if (dateEndChattingFromStore?.length) return dateEndChattingFromStore;
|
|
if (balance?.maxFinishedAt) return balance.maxFinishedAt;
|
|
if (session?.maxFinishedAt) return session.maxFinishedAt;
|
|
return "2000-01-01T00:00:00.290Z"
|
|
}, [balance, dateEndChattingFromStore, session]);
|
|
|
|
const messagesAfterEnd = useMemo(() => {
|
|
return messages.filter((message) => compareDates(message.createdDate, dateEndChatting) >= 0)
|
|
}, [dateEndChatting, messages])
|
|
|
|
const isAvailableChattingByMessages = useMemo(() => {
|
|
return !messagesAfterEnd.find((message) => message.role === "assistant") || isAvailableChatting
|
|
}, [isAvailableChatting, messagesAfterEnd])
|
|
|
|
// send
|
|
const joinChat = useCallback((chatId: string) => {
|
|
if (chatId && socket) {
|
|
socket.emit('join_chat', {
|
|
chatId
|
|
});
|
|
}
|
|
}, [socket]);
|
|
|
|
const startSession = useCallback((chatId: string) => {
|
|
if (chatId && socket && !session) {
|
|
socket.emit('start_session', { chatId });
|
|
}
|
|
}, [session, socket]);
|
|
|
|
const sendMessage = useCallback((message: string) => {
|
|
if (chatId && socket) {
|
|
setIsLoadingAdvisorMessage(true);
|
|
setIsLoadingSelfMessage(true);
|
|
socket.emit('send_message', { chatId, message });
|
|
}
|
|
}, [chatId, socket]);
|
|
|
|
const readMessage = useCallback((messagesId: string[]) => {
|
|
if (chatId && socket) {
|
|
socket.emit('read_message', { messages: messagesId });
|
|
}
|
|
}, [chatId, socket]);
|
|
|
|
const endSession = useCallback((chatId: string) => {
|
|
if (chatId && socket) {
|
|
socket.emit('end_session', { chatId });
|
|
}
|
|
}, [socket]);
|
|
|
|
const leaveChat = useCallback((chatId: string) => {
|
|
if (chatId && socket) {
|
|
socket.emit('leave_chat', { chatId });
|
|
}
|
|
}, [socket]);
|
|
|
|
const fetchBalance = useCallback((chatId: string) => {
|
|
if (chatId && socket) {
|
|
socket.emit('fetch_balance', { chatId });
|
|
}
|
|
}, [socket]);
|
|
|
|
|
|
// receive
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const chatJoined = useCallback((_response: IResponse<null>) => {
|
|
console.log("chatJoined");
|
|
}, [])
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const chatLeft = useCallback((_response: IResponse<boolean>) => {
|
|
console.log("chatLeft");
|
|
}, [])
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const balanceUpdated = useCallback((_response: IResponse<IBalanceUpdated>) => {
|
|
fetchBalance(chatId);
|
|
}, [chatId, fetchBalance])
|
|
|
|
const receiveMessage = useCallback((message: IMessage[]) => {
|
|
if (!message?.[0]) return;
|
|
|
|
if (message[0].role === "user") {
|
|
setIsLoadingSelfMessage(false);
|
|
}
|
|
if (message[0].role === "assistant") {
|
|
setIsLoadingAdvisorMessage(false);
|
|
}
|
|
setMessages((prev) => {
|
|
if (prev.some(msg => msg.id === message[0].id)) {
|
|
return prev;
|
|
}
|
|
return [...prev, message[0]];
|
|
});
|
|
}, []);
|
|
|
|
const currentBalance = useCallback((balance: IResponse<ICurrentBalance>) => {
|
|
setBalance(balance?.data);
|
|
}, [])
|
|
|
|
const sessionStarted = useCallback((response: IResponse<ISessionStarted>) => {
|
|
setSession(response?.data);
|
|
}, [])
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const sessionEnded = useCallback((_response: IResponse<boolean>) => {
|
|
setSession(null);
|
|
}, [])
|
|
|
|
// logic
|
|
const init = useCallback(async () => {
|
|
if (!chatId || !userId) {
|
|
return;
|
|
}
|
|
|
|
const messages = await api.getChatMessages({
|
|
token,
|
|
chatId
|
|
})
|
|
|
|
// routes.server.chatSocket()
|
|
const newSocket = io(routes.server.chatSocket(), {
|
|
query: { userId }
|
|
});
|
|
|
|
setSocket(newSocket);
|
|
|
|
setMessages(messages?.messages || []);
|
|
|
|
newSocket.on('chat_joined', (response: IResponse<null>) => {
|
|
chatJoined(response);
|
|
});
|
|
|
|
newSocket.on('receive_message', (message: IMessage[]) => {
|
|
receiveMessage(message);
|
|
});
|
|
|
|
newSocket.on('current_balance', (balance: IResponse<ICurrentBalance>) => {
|
|
currentBalance(balance);
|
|
});
|
|
|
|
newSocket.on('session_started', (response: IResponse<ISessionStarted>) => {
|
|
sessionStarted(response);
|
|
});
|
|
|
|
newSocket.on('session_ended', (response: IResponse<boolean>) => {
|
|
sessionEnded(response);
|
|
});
|
|
|
|
newSocket.on('chat_left', (response: IResponse<boolean>) => {
|
|
chatLeft(response);
|
|
});
|
|
|
|
newSocket.on('balance_updated', (response: IResponse<IBalanceUpdated>) => {
|
|
balanceUpdated(response);
|
|
});
|
|
|
|
return () => {
|
|
newSocket.disconnect();
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
setIsLoading(true);
|
|
init();
|
|
}, [init]);
|
|
|
|
// useEffect(() => {
|
|
// if (!socket) return setIsAvailableChatting(false);
|
|
// joinChat(chatId);
|
|
// fetchBalance(chatId);
|
|
// }, [chatId, fetchBalance, joinChat, socket]);
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
fetchBalance(chatId);
|
|
}, 5000);
|
|
return () => clearInterval(interval);
|
|
}, [chatId, fetchBalance, session]);
|
|
|
|
useEffect(() => {
|
|
if (!balance) return setIsAvailableChatting(false);
|
|
if (initialBalance === null) setInitialBalance(balance.balance);
|
|
if (balance.balance > 0 && !session) {
|
|
startSession(chatId);
|
|
}
|
|
if (balance.balance <= 0 && session) {
|
|
endSession(chatId);
|
|
}
|
|
setIsLoading(false);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [balance, chatId, endSession, session, startSession]);
|
|
|
|
useEffect(() => {
|
|
if (!session) return setIsAvailableChatting(false);
|
|
setIsLoading(true);
|
|
const checkIsAvailableChatting: () => Promise<void> = async () => {
|
|
const isAvailableDate = isAvailableDateUsing(session?.maxFinishedAt);
|
|
if (isAvailableDate) {
|
|
setIsAvailableChatting(true);
|
|
setIsLoading(false);
|
|
await sleep(1000);
|
|
return checkIsAvailableChatting();
|
|
}
|
|
setIsAvailableChatting(false);
|
|
setIsLoading(false);
|
|
}
|
|
|
|
checkIsAvailableChatting();
|
|
}, [chatId, endSession, session])
|
|
|
|
useEffect(() => {
|
|
if (session?.maxFinishedAt?.length) {
|
|
dispatch(actions.chat.updateDateEndChatting(session.maxFinishedAt));
|
|
}
|
|
}, [dispatch, session]);
|
|
|
|
useEffect(() => {
|
|
if (!socket) return;
|
|
|
|
const handleReconnect = () => {
|
|
setIsLoadingSelfMessage(false);
|
|
setIsLoadingAdvisorMessage(false);
|
|
joinChat(chatId);
|
|
fetchBalance(chatId);
|
|
};
|
|
|
|
socket.on('connect', handleReconnect);
|
|
socket.on('disconnect', () => {
|
|
setIsLoadingSelfMessage(false);
|
|
setIsLoadingAdvisorMessage(false);
|
|
});
|
|
|
|
return () => {
|
|
socket.off('connect', handleReconnect);
|
|
socket.off('disconnect');
|
|
};
|
|
}, [socket, chatId, joinChat, fetchBalance]);
|
|
|
|
// clean up
|
|
useEffect(() => {
|
|
const handleBeforeUnload = () => {
|
|
if (socket) {
|
|
endSession(chatId);
|
|
leaveChat(chatId);
|
|
socket.disconnect();
|
|
}
|
|
};
|
|
|
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
|
|
return () => {
|
|
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
handleBeforeUnload();
|
|
};
|
|
}, [socket, chatId, endSession, leaveChat]);
|
|
|
|
return useMemo(() => ({
|
|
isLoading,
|
|
isLoadingSelfMessage,
|
|
isLoadingAdvisorMessage,
|
|
socket,
|
|
messages,
|
|
balance,
|
|
session,
|
|
isAvailableChatting,
|
|
messagesAfterEnd,
|
|
isAvailableChattingByMessages,
|
|
initialBalance,
|
|
sendMessage,
|
|
readMessage
|
|
}), [
|
|
isLoading,
|
|
isLoadingSelfMessage,
|
|
isLoadingAdvisorMessage,
|
|
socket,
|
|
messages,
|
|
balance,
|
|
session,
|
|
isAvailableChatting,
|
|
messagesAfterEnd,
|
|
isAvailableChattingByMessages,
|
|
initialBalance,
|
|
sendMessage,
|
|
readMessage
|
|
]);
|
|
};
|
|
|
|
export default useChatSocket;
|