w-aura/src/components/ChatsPath/pages/ExpertChat/index.tsx
Daniil Chemerkin f1a5b30650 Develop
2024-11-20 13:21:04 +00:00

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;