387 lines
12 KiB
TypeScript
387 lines
12 KiB
TypeScript
import { useNavigate } from "react-router-dom";
|
|
import styles from "./styles.module.scss";
|
|
import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react";
|
|
import Loader, { LoaderColor } from "@/components/Loader";
|
|
import ChatHeader from "./components/ChatHeader";
|
|
import InputMessage from "./components/InputMessage";
|
|
import routes from "@/routes";
|
|
import Message from "./components/Message";
|
|
import LoaderDots from "./components/LoaderDots";
|
|
import StartInfo from "./components/StartInfo";
|
|
import RefillCreditsModal from "./components/RefillCreditsModal";
|
|
import useChatSocket from "@/hooks/chatsSocket/useChatsSocket";
|
|
import { useDispatch, useSelector } from "react-redux";
|
|
import { actions, selectors } from "@/store";
|
|
import BottomModal from "../../components/BottomModal";
|
|
import OutOfCreditsModal from "./components/OutOfCreditsModal";
|
|
import RefillProductsModal from "./components/RefillProductsModal";
|
|
import { Products, useApi, useApiCall } from "@/api";
|
|
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
|
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
|
import { useAuth } from "@/auth";
|
|
import { ResponsePost } from "@/api/resources/SinglePayment";
|
|
import { createSinglePayment } from "@/services/singlePayment";
|
|
import Modal from "@/components/Modal";
|
|
import Title from "@/components/Title";
|
|
import PaymentForm from "@/components/pages/SinglePaymentPage/PaymentForm";
|
|
import { getPriceCentsToDollars } from "@/services/price";
|
|
import { IMessage } from "@/api/resources/ChatMessages";
|
|
|
|
const returnUrl = `${window.location.protocol}//${
|
|
window.location.host
|
|
}${routes.client.chatsExpert()}`;
|
|
|
|
function ExpertChat() {
|
|
const api = useApi();
|
|
const dispatch = useDispatch();
|
|
const navigate = useNavigate();
|
|
const assistant = useSelector(selectors.selectCurrentAssistant);
|
|
const chatId = useSelector(selectors.selectCurrentChatId);
|
|
const userId = useSelector(selectors.selectUserId);
|
|
const [messageText, setMessageText] = useState("");
|
|
const [textareaRows, setTextareaRows] = useState(1);
|
|
// const [isLoadingLatestMessages, setIsLoadingLatestMessages] = useState(false);
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
const [isShowRefillCreditsModal, setIsShowRefillCreditsModal] =
|
|
useState(false);
|
|
const [isShowOutOfCreditsModal, setIsShowOutOfCreditsModal] = useState(false);
|
|
const [isShowRefillProductsModal, setIsShowRefillProductsModal] =
|
|
useState(false);
|
|
|
|
const {
|
|
isLoading,
|
|
isLoadingSelfMessage,
|
|
isLoadingAdvisorMessage,
|
|
messages,
|
|
isAvailableChatting,
|
|
messagesAfterEnd,
|
|
initialBalance,
|
|
sendMessage,
|
|
readMessage,
|
|
} = useChatSocket(userId, chatId);
|
|
|
|
// Payment
|
|
const { user: userFromStore } = useAuth();
|
|
const tokenFromStore = useSelector(selectors.selectToken);
|
|
const [paymentIntent, setPaymentIntent] = useState<ResponsePost | null>(null);
|
|
const [isLoadingPayment, setIsLoadingPayment] = useState(false);
|
|
const [isError, setIsError] = useState(false);
|
|
const [currentProduct, setCurrentProduct] = useState<IPaywallProduct | null>(
|
|
null
|
|
);
|
|
|
|
const isPayedFirstPurchase = useSelector(
|
|
selectors.selectIsPayedFirstPurchase
|
|
);
|
|
|
|
const checkIsPayedFirstPurchase = useCallback(async () => {
|
|
if (isPayedFirstPurchase) return;
|
|
const isPayed = await api.checkProductPurchased({
|
|
token: tokenFromStore,
|
|
productKey: "credits.100",
|
|
});
|
|
if (isPayed && "active" in isPayed && isPayed.active) {
|
|
dispatch(actions.chat.updateIsPayedFirstPurchase(true));
|
|
}
|
|
return isPayed;
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [
|
|
api,
|
|
dispatch,
|
|
isPayedFirstPurchase,
|
|
tokenFromStore,
|
|
isAvailableChatting,
|
|
]);
|
|
|
|
const { data: isPayedFirstPurchaseResponse } = useApiCall<
|
|
Products.ResponseGet | undefined
|
|
>(checkIsPayedFirstPurchase);
|
|
|
|
const { products } = usePaywall({
|
|
placementKey: EPlacementKeys["aura.placement.chat"],
|
|
});
|
|
|
|
const scrollToBottom = () => {
|
|
setTimeout(() => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
}, 100);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!isLoading && !!messages.length) scrollToBottom();
|
|
}, [messages, isLoading]);
|
|
|
|
const handleChangeMessageText = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
const { scrollHeight, clientHeight, value } = e.target;
|
|
if (
|
|
scrollHeight > clientHeight &&
|
|
textareaRows < 5 &&
|
|
value.length > messageText.length
|
|
) {
|
|
setTextareaRows((prev) => prev + 1);
|
|
}
|
|
if (
|
|
scrollHeight === clientHeight &&
|
|
textareaRows > 1 &&
|
|
value.length < messageText.length
|
|
) {
|
|
setTextareaRows((prev) => prev - 1);
|
|
}
|
|
setMessageText(e.target.value);
|
|
};
|
|
|
|
const getIsSelfMessage = (role: string) => role === "user";
|
|
|
|
const deleteDataFromMessage = (messageText: string) => {
|
|
const splittedText = messageText?.split("#");
|
|
if (splittedText?.length > 2) {
|
|
return splittedText?.slice(2).join("#");
|
|
}
|
|
return messageText;
|
|
};
|
|
|
|
const endChat = () => {
|
|
navigate(routes.client.chatsCategories());
|
|
};
|
|
|
|
const handleRefillModalClickButton = () => {
|
|
createPayment("credits.100");
|
|
};
|
|
|
|
const handleOutOfCreditsModalClickButton = () => {
|
|
createPayment("credits.80");
|
|
};
|
|
|
|
const handleRefillProductsModalClickButton = (productKey: string) => {
|
|
createPayment(productKey);
|
|
};
|
|
|
|
const handleSendMessage = (messageText: string) => {
|
|
if (!isAvailableChatting) {
|
|
showModals();
|
|
}
|
|
setMessageText("");
|
|
sendMessage(messageText);
|
|
};
|
|
|
|
const closeModals = useCallback(() => {
|
|
setIsShowRefillProductsModal(false);
|
|
setIsShowOutOfCreditsModal(false);
|
|
setIsShowRefillCreditsModal(false);
|
|
}, []);
|
|
|
|
const createPayment = useCallback(
|
|
async (productKey: string) => {
|
|
try {
|
|
if (!userFromStore || !productKey.length) return;
|
|
const currentProduct =
|
|
products?.find((product) => product.key === productKey) || null;
|
|
if (!currentProduct) return;
|
|
setCurrentProduct(currentProduct);
|
|
setIsLoadingPayment(true);
|
|
const { _id, key } = currentProduct;
|
|
const paymentInfo = {
|
|
productId: _id,
|
|
key,
|
|
};
|
|
const paymentIntent = await createSinglePayment(
|
|
userFromStore,
|
|
paymentInfo,
|
|
tokenFromStore,
|
|
userFromStore.email,
|
|
userFromStore.profile.full_name,
|
|
userFromStore.profile.birthday,
|
|
returnUrl,
|
|
api
|
|
);
|
|
setPaymentIntent(paymentIntent);
|
|
if ("payment" in paymentIntent) {
|
|
if (paymentIntent.payment.status === "paid") return closeModals();
|
|
return setIsError(true);
|
|
}
|
|
} catch (error) {
|
|
console.log(error);
|
|
|
|
setIsError(true);
|
|
} finally {
|
|
setIsLoadingPayment(false);
|
|
}
|
|
},
|
|
[api, closeModals, products, tokenFromStore, userFromStore]
|
|
);
|
|
|
|
const showModals = useCallback(() => {
|
|
if (isPayedFirstPurchase) {
|
|
return setIsShowOutOfCreditsModal(true);
|
|
}
|
|
|
|
if (
|
|
isPayedFirstPurchase ||
|
|
!isPayedFirstPurchaseResponse ||
|
|
!("active" in isPayedFirstPurchaseResponse) ||
|
|
!isPayedFirstPurchaseResponse
|
|
) {
|
|
return setIsShowOutOfCreditsModal(true);
|
|
}
|
|
|
|
return setIsShowRefillCreditsModal(true);
|
|
}, [isPayedFirstPurchase, isPayedFirstPurchaseResponse]);
|
|
|
|
useEffect(() => {
|
|
if (isAvailableChatting && !isLoading) {
|
|
closeModals();
|
|
}
|
|
}, [closeModals, isAvailableChatting, isLoading]);
|
|
|
|
useEffect(() => {
|
|
if (initialBalance !== null && !isLoading && !initialBalance) {
|
|
showModals();
|
|
}
|
|
}, [initialBalance, isLoading, showModals]);
|
|
|
|
const isBlurMessage = (message: IMessage) => {
|
|
return (
|
|
!!messagesAfterEnd.find((item) => item.id === message.id) &&
|
|
message.role === "assistant"
|
|
);
|
|
};
|
|
|
|
return (
|
|
<section className={`${styles.page} page`}>
|
|
{!isLoading &&
|
|
paymentIntent &&
|
|
"paymentIntent" in paymentIntent &&
|
|
!!tokenFromStore.length &&
|
|
currentProduct && (
|
|
<>
|
|
<Modal
|
|
open={!!paymentIntent}
|
|
onClose={() => setPaymentIntent(null)}
|
|
containerClassName={styles.modal}
|
|
>
|
|
<Title variant="h1" className={styles["modal-title"]}>
|
|
{getPriceCentsToDollars(currentProduct.price || 0)}$
|
|
</Title>
|
|
<PaymentForm
|
|
isLoadingPayment={isLoadingPayment}
|
|
stripePublicKey={paymentIntent.paymentIntent.data.public_key}
|
|
clientSecret={paymentIntent.paymentIntent.data.client_secret}
|
|
returnUrl={returnUrl}
|
|
/>
|
|
</Modal>
|
|
</>
|
|
)}
|
|
{isLoading && (
|
|
<Loader color={LoaderColor.Red} className={styles.loader} />
|
|
)}
|
|
{!isLoading && (
|
|
<ChatHeader
|
|
name={assistant?.name || ""}
|
|
avatar={assistant?.image || ""}
|
|
classNameContainer={styles["header-container"]}
|
|
clickBackButton={endChat}
|
|
isTimerGoing={isAvailableChatting}
|
|
hasBackButton={false}
|
|
/>
|
|
)}
|
|
{!isLoading && !!messages.length && (
|
|
<div className={styles["messages-container"]}>
|
|
{messages.map((message) => (
|
|
<Message
|
|
avatar={assistant?.image || ""}
|
|
text={deleteDataFromMessage(message.text)}
|
|
isVisible={deleteDataFromMessage(message.text) !== " HI"}
|
|
advisorName={assistant?.name || ""}
|
|
backgroundTextColor={
|
|
getIsSelfMessage(message.role) ? "#0080ff" : "#c9c9c9"
|
|
}
|
|
textColor={getIsSelfMessage(message.role) ? "#fff" : "#000"}
|
|
isSelf={getIsSelfMessage(message.role)}
|
|
key={message.id}
|
|
isRead={message.isRead}
|
|
readMessage={readMessage}
|
|
messageId={message.id}
|
|
isBlur={isBlurMessage(message)}
|
|
onClickBlur={() => showModals()}
|
|
/>
|
|
))}
|
|
{isLoadingAdvisorMessage && !isLoadingSelfMessage && (
|
|
<Message
|
|
avatar={assistant?.image || ""}
|
|
text={<LoaderDots />}
|
|
isSelf={false}
|
|
backgroundTextColor={"#c9c9c9"}
|
|
textColor={"#000"}
|
|
/>
|
|
)}
|
|
{/* <div className={styles["loader-container"]}>
|
|
{isLoadingLatestMessages && <Loader color={LoaderColor.Red} />}
|
|
</div> */}
|
|
</div>
|
|
)}
|
|
|
|
{!messages.length && !isLoading && <StartInfo />}
|
|
|
|
<div ref={messagesEndRef} />
|
|
{!isLoading && (
|
|
<InputMessage
|
|
placeholder="Text message"
|
|
messageText={messageText}
|
|
textareaRows={textareaRows}
|
|
disabledTextArea={isLoadingAdvisorMessage || isLoadingSelfMessage}
|
|
disabledButton={!messageText.length}
|
|
classNameContainer={styles["input-container"]}
|
|
handleChangeMessageText={handleChangeMessageText}
|
|
isLoading={isLoadingSelfMessage}
|
|
submitForm={handleSendMessage}
|
|
description={`The cost of chat is ${assistant?.price} credits/min`}
|
|
/>
|
|
)}
|
|
{!isLoading && (
|
|
<>
|
|
{isShowRefillCreditsModal && (
|
|
<BottomModal handleClose={() => setIsShowRefillCreditsModal(false)}>
|
|
<RefillCreditsModal
|
|
isError={isError}
|
|
isLoading={isLoadingPayment}
|
|
products={products}
|
|
handleClickButton={handleRefillModalClickButton}
|
|
/>
|
|
</BottomModal>
|
|
)}
|
|
{isShowOutOfCreditsModal && (
|
|
<BottomModal handleClose={() => setIsShowOutOfCreditsModal(false)}>
|
|
<OutOfCreditsModal
|
|
isError={isError}
|
|
isLoading={isLoadingPayment}
|
|
products={products}
|
|
handleClickButton={handleOutOfCreditsModalClickButton}
|
|
handleClickNotWant={endChat}
|
|
timerLeft={() => {
|
|
if (isLoadingPayment) return;
|
|
setIsShowOutOfCreditsModal(false);
|
|
setIsShowRefillProductsModal(true);
|
|
}}
|
|
/>
|
|
</BottomModal>
|
|
)}
|
|
{isShowRefillProductsModal && (
|
|
<BottomModal
|
|
handleClose={() => setIsShowRefillProductsModal(false)}
|
|
>
|
|
<RefillProductsModal
|
|
isError={isError}
|
|
isLoading={isLoadingPayment}
|
|
products={products}
|
|
handleClickButton={handleRefillProductsModalClickButton}
|
|
/>
|
|
</BottomModal>
|
|
)}
|
|
</>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export default ExpertChat;
|