Добавил блюр на сообщения и блокировку при нулевом балансе
This commit is contained in:
parent
9fbfa63fea
commit
c5002e1a7a
@ -13,11 +13,13 @@ import styles from "./CategoryChats.module.scss";
|
||||
interface CategoryChatsProps {
|
||||
chats: IChat[];
|
||||
maxVisibleChats?: number;
|
||||
currentBalance?: number;
|
||||
}
|
||||
|
||||
export default function CategoryChats({
|
||||
chats,
|
||||
maxVisibleChats = 3,
|
||||
currentBalance = 0,
|
||||
}: CategoryChatsProps) {
|
||||
const router = useRouter();
|
||||
const setCurrentChat = useChatStore(state => state.setCurrentChat);
|
||||
@ -40,7 +42,9 @@ export default function CategoryChats({
|
||||
message: {
|
||||
type: chat.lastMessage.type,
|
||||
content: chat.lastMessage.text,
|
||||
sentWithZeroBalance: chat.lastMessage.sentWithZeroBalance,
|
||||
},
|
||||
currentBalance: currentBalance,
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
|
||||
import { Skeleton } from "@/components/ui";
|
||||
import { Chips } from "@/components/widgets";
|
||||
import { useBalance } from "@/hooks/balance/useBalance";
|
||||
import { useChats } from "@/providers/chats-provider";
|
||||
|
||||
import { CategoryChats, ChatItemsList } from "..";
|
||||
@ -12,6 +13,7 @@ const MAX_HIDE_VISIBLE_COUNT = 3;
|
||||
|
||||
export default function ChatCategories() {
|
||||
const { categorizedChats } = useChats();
|
||||
const { balance } = useBalance();
|
||||
|
||||
const [activeChip, setActiveChip] = useState<string>("All");
|
||||
const [maxVisibleChats, setMaxVisibleChats] = useState<
|
||||
@ -59,6 +61,7 @@ export default function ChatCategories() {
|
||||
<CategoryChats
|
||||
chats={categorizedChats[key]}
|
||||
maxVisibleChats={maxVisibleChats[key] ?? MAX_HIDE_VISIBLE_COUNT}
|
||||
currentBalance={balance}
|
||||
/>
|
||||
</ChatItemsList>
|
||||
))}
|
||||
|
||||
@ -10,4 +10,5 @@
|
||||
align-self: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import clsx from "clsx";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { IChatMessage } from "@/entities/chats/types";
|
||||
import type { IChatMessage } from "@/entities/chats/types";
|
||||
import { useShowRefillModal } from "@/hooks/refill/useShowRefillModal";
|
||||
import { useChat } from "@/providers/chat-provider";
|
||||
import { formatTime } from "@/shared/utils/date";
|
||||
|
||||
import styles from "./ChatMessage.module.scss";
|
||||
import MessageBubble from "./MessageBubble/MessageBubble";
|
||||
import MessageMeta from "./MessageMeta/MessageMeta";
|
||||
import MessageStatus from "./MessageStatus/MessageStatus";
|
||||
import MessageText from "./MessageText/MessageText";
|
||||
import MessageTyping from "./MessageTyping/MessageTyping";
|
||||
|
||||
import styles from "./ChatMessage.module.scss";
|
||||
|
||||
export interface ChatMessageProps {
|
||||
// message: {
|
||||
// id: string;
|
||||
@ -31,9 +31,22 @@ export interface ChatMessageProps {
|
||||
}
|
||||
|
||||
export default function ChatMessage({ message }: ChatMessageProps) {
|
||||
const { isConnected, read } = useChat();
|
||||
const { isConnected, read, balance } = useChat();
|
||||
const { showRefillModal } = useShowRefillModal();
|
||||
const isOwn = message.role === "user";
|
||||
|
||||
// Применяем блюр к ОТВЕТАМ АССИСТЕНТА если они были отправлены при нулевом балансе И текущий баланс все еще нулевой
|
||||
const shouldBlur =
|
||||
!isOwn && // Блюрим только сообщения ассистента (НЕ пользователя)
|
||||
message.sentWithZeroBalance === true &&
|
||||
(balance?.balance ?? 0) <= 0;
|
||||
|
||||
const handleBlurClick = () => {
|
||||
if (message.chatId) {
|
||||
showRefillModal(message.chatId);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!!message.id &&
|
||||
@ -50,7 +63,12 @@ export default function ChatMessage({ message }: ChatMessageProps) {
|
||||
<div className={clsx(styles.message, isOwn && styles.own)}>
|
||||
<MessageBubble isOwn={isOwn}>
|
||||
{message.type === "text" && message.id !== "typing" && (
|
||||
<MessageText text={message.text} isOwn={isOwn} />
|
||||
<MessageText
|
||||
text={message.text}
|
||||
isOwn={isOwn}
|
||||
isBlurred={shouldBlur}
|
||||
onBlurClick={handleBlurClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
{message.id === "typing" && <MessageTyping />}
|
||||
|
||||
@ -6,4 +6,15 @@
|
||||
&.own {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.blurred {
|
||||
filter: blur(4px);
|
||||
opacity: 0.6;
|
||||
transition: filter 0.3s ease, opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
user-select: none; // Запрещаем выделение текста
|
||||
-webkit-user-select: none; // Safari
|
||||
-moz-user-select: none; // Firefox
|
||||
-ms-user-select: none; // IE/Edge
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,24 +8,40 @@ interface MessageTextProps {
|
||||
text?: string;
|
||||
isOwn: boolean;
|
||||
className?: string;
|
||||
isBlurred?: boolean;
|
||||
onBlurClick?: () => void;
|
||||
}
|
||||
|
||||
export default function MessageText({
|
||||
text,
|
||||
isOwn,
|
||||
className,
|
||||
isBlurred = false,
|
||||
onBlurClick,
|
||||
}: MessageTextProps) {
|
||||
const handleClick = () => {
|
||||
if (isBlurred && onBlurClick) {
|
||||
onBlurClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Typography
|
||||
as="p"
|
||||
align="left"
|
||||
className={clsx(
|
||||
styles.text,
|
||||
isOwn ? styles.own : styles.other,
|
||||
className
|
||||
)}
|
||||
<div
|
||||
onClick={handleClick}
|
||||
style={isBlurred ? { cursor: 'pointer' } : undefined}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
<Typography
|
||||
as="p"
|
||||
align="left"
|
||||
className={clsx(
|
||||
styles.text,
|
||||
isOwn ? styles.own : styles.other,
|
||||
isBlurred && styles.blurred,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,38 +6,65 @@ import { useTranslations } from "next-intl";
|
||||
import { ModalSheet } from "@/components/ui";
|
||||
import { useChat } from "@/providers/chat-provider";
|
||||
import { useToast } from "@/providers/toast-provider";
|
||||
import { useSocketEmit } from "@/services/socket";
|
||||
|
||||
import { RefillOptionsModal, RefillTimerModal } from "..";
|
||||
|
||||
export default function ChatModalsWrapper() {
|
||||
const t = useTranslations("Chat");
|
||||
const { refillModals } = useChat();
|
||||
const { refillModals, clearRefillModals } = useChat();
|
||||
const { addToast } = useToast();
|
||||
const emit = useSocketEmit();
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [modalChild, setModalChild] = useState<
|
||||
"refill-timer" | "refill-options" | null
|
||||
>(null);
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
// Теперь hasShownTimerModal приходит с сервера
|
||||
const hasShownTimerModal = refillModals?.hasShownTimerModal ?? false;
|
||||
|
||||
const handleModalClose = () => {
|
||||
setIsClosing(true);
|
||||
setIsModalOpen(false);
|
||||
const timeout = setTimeout(() => {
|
||||
setModalChild(null);
|
||||
setIsClosing(false);
|
||||
clearRefillModals(); // Очищаем состояние refillModals
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
};
|
||||
|
||||
const handleTimerModalClose = () => {
|
||||
// При закрытии крестика в timer modal - переходим к options modal
|
||||
setModalChild("refill-options");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!!refillModals?.oneClick) {
|
||||
// Не обрабатываем новые события если модальное окно в процессе закрытия
|
||||
if (isClosing) return;
|
||||
|
||||
if (!!refillModals?.oneClick && !hasShownTimerModal) {
|
||||
setIsModalOpen(true);
|
||||
return setModalChild("refill-timer");
|
||||
setModalChild("refill-timer");
|
||||
// Подтверждаем серверу что показали timer modal
|
||||
emit("confirm_timer_modal_shown");
|
||||
return;
|
||||
}
|
||||
if (!!refillModals?.products) {
|
||||
setIsModalOpen(true);
|
||||
return setModalChild("refill-options");
|
||||
}
|
||||
}, [refillModals]);
|
||||
// Если получили событие повторно и timer modal уже показывался,
|
||||
// сразу показываем options modal
|
||||
if (!!refillModals?.oneClick && hasShownTimerModal) {
|
||||
if (!isModalOpen) {
|
||||
setIsModalOpen(true);
|
||||
return setModalChild("refill-options");
|
||||
}
|
||||
}
|
||||
}, [refillModals, hasShownTimerModal, isModalOpen, emit, isClosing]);
|
||||
|
||||
const handlePaymentSuccess = () => {
|
||||
handleModalClose();
|
||||
@ -54,8 +81,9 @@ export default function ChatModalsWrapper() {
|
||||
return (
|
||||
<ModalSheet
|
||||
open={isModalOpen}
|
||||
onClose={handleModalClose}
|
||||
onClose={modalChild === "refill-timer" ? handleTimerModalClose : handleModalClose}
|
||||
variant={modalChild === "refill-timer" ? "gray" : "white"}
|
||||
disableBackdropClose={true}
|
||||
>
|
||||
{modalChild === "refill-timer" && !!refillModals?.oneClick && (
|
||||
<RefillTimerModal
|
||||
|
||||
@ -20,14 +20,16 @@ const getTopPositionItem = (index: number) => {
|
||||
|
||||
interface CorrespondenceStartedProps {
|
||||
chats: IChat[];
|
||||
isVisibleAll?: boolean;
|
||||
isVisibleAll: boolean;
|
||||
maxHideVisibleCount?: number;
|
||||
currentBalance?: number;
|
||||
}
|
||||
|
||||
export default function CorrespondenceStarted({
|
||||
chats,
|
||||
isVisibleAll = false,
|
||||
maxHideVisibleCount = 3,
|
||||
currentBalance = 0,
|
||||
}: CorrespondenceStartedProps) {
|
||||
const router = useRouter();
|
||||
const setCurrentChat = useChatStore(state => state.setCurrentChat);
|
||||
@ -61,7 +63,9 @@ export default function CorrespondenceStarted({
|
||||
message: {
|
||||
type: chat.lastMessage.type,
|
||||
content: chat.lastMessage.text,
|
||||
sentWithZeroBalance: chat.lastMessage.sentWithZeroBalance,
|
||||
},
|
||||
currentBalance: currentBalance,
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Skeleton } from "@/components/ui";
|
||||
import { useBalance } from "@/hooks/balance/useBalance";
|
||||
import { useAppUiStore } from "@/providers/app-ui-store-provider";
|
||||
import { useChats } from "@/providers/chats-provider";
|
||||
|
||||
@ -11,6 +12,7 @@ import { ChatItemsList, CorrespondenceStarted } from "..";
|
||||
export default function CorrespondenceStartedWrapper() {
|
||||
const t = useTranslations("Chat");
|
||||
const { startedChats } = useChats();
|
||||
const { balance } = useBalance();
|
||||
|
||||
const { isVisibleAll } = useAppUiStore(
|
||||
state => state.chats.correspondenceStarted
|
||||
@ -41,6 +43,7 @@ export default function CorrespondenceStartedWrapper() {
|
||||
<CorrespondenceStarted
|
||||
chats={startedChats}
|
||||
isVisibleAll={isVisibleAll}
|
||||
currentBalance={balance}
|
||||
/>
|
||||
</ChatItemsList>
|
||||
)}
|
||||
|
||||
@ -5,6 +5,7 @@ import { useLocale } from "next-intl";
|
||||
|
||||
import NewMessages from "@/components/domains/chat/NewMessages/NewMessages";
|
||||
import ViewAll from "@/components/domains/chat/ViewAll/ViewAll";
|
||||
import { useBalance } from "@/hooks/balance/useBalance";
|
||||
import { useAppUiStore } from "@/providers/app-ui-store-provider";
|
||||
import { useChats } from "@/providers/chats-provider";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
@ -14,6 +15,7 @@ import styles from "./GlobalNewMessagesBanner.module.scss";
|
||||
|
||||
export default function GlobalNewMessagesBanner() {
|
||||
const { unreadChats } = useChats();
|
||||
const { balance } = useBalance();
|
||||
|
||||
// Exclude banner on chat-related, settings (profile), and retention funnel pages
|
||||
const pathname = usePathname();
|
||||
@ -39,7 +41,7 @@ export default function GlobalNewMessagesBanner() {
|
||||
onClick={() => setHomeNewMessages({ isVisibleAll: !isVisibleAll })}
|
||||
/>
|
||||
)}
|
||||
<NewMessages chats={unreadChats} isVisibleAll={isVisibleAll} />
|
||||
<NewMessages chats={unreadChats} isVisibleAll={isVisibleAll} currentBalance={balance} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -17,4 +17,14 @@
|
||||
-webkit-box-orient: vertical;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
|
||||
&.blurred {
|
||||
filter: blur(4px);
|
||||
opacity: 0.6;
|
||||
transition: filter 0.3s ease, opacity 0.3s ease;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,18 +8,26 @@ export interface LastMessagePreviewProps {
|
||||
message: {
|
||||
type: "text" | "voice" | "image";
|
||||
content?: string;
|
||||
sentWithZeroBalance?: boolean;
|
||||
};
|
||||
isTyping?: boolean;
|
||||
isRead?: boolean;
|
||||
currentBalance?: number;
|
||||
}
|
||||
|
||||
export default function LastMessagePreview({
|
||||
message,
|
||||
isTyping,
|
||||
isRead,
|
||||
currentBalance = 0,
|
||||
}: LastMessagePreviewProps) {
|
||||
const t = useTranslations("Chat");
|
||||
|
||||
// Определяем нужно ли блюрить сообщение
|
||||
const shouldBlur =
|
||||
message.sentWithZeroBalance === true &&
|
||||
currentBalance <= 0;
|
||||
|
||||
const getMessageIcon = () => {
|
||||
switch (message.type) {
|
||||
case "voice":
|
||||
@ -72,7 +80,7 @@ export default function LastMessagePreview({
|
||||
size="sm"
|
||||
color="secondary"
|
||||
align="left"
|
||||
className={styles.text}
|
||||
className={`${styles.text} ${shouldBlur ? styles.blurred : ''}`}
|
||||
>
|
||||
{getMessageText()}
|
||||
</Typography>
|
||||
|
||||
@ -9,14 +9,15 @@ import styles from "./MessageInput.module.scss";
|
||||
|
||||
interface MessageInputProps {
|
||||
onSend: (message: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function MessageInput({ onSend }: MessageInputProps) {
|
||||
export default function MessageInput({ onSend, disabled = false }: MessageInputProps) {
|
||||
const t = useTranslations("Chat");
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const handleSend = () => {
|
||||
if (message.trim()) {
|
||||
if (message.trim() && !disabled) {
|
||||
onSend(message.trim());
|
||||
setMessage("");
|
||||
}
|
||||
@ -57,7 +58,7 @@ export default function MessageInput({ onSend }: MessageInputProps) {
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
disabled={!message.trim()}
|
||||
disabled={!message.trim() || disabled}
|
||||
aria-label="Send"
|
||||
className={styles.sendButton}
|
||||
>
|
||||
|
||||
@ -7,12 +7,15 @@ import { MessageInput } from "..";
|
||||
import styles from "./MessageInputWrapper.module.scss";
|
||||
|
||||
export default function MessageInputWrapper() {
|
||||
const { send } = useChat();
|
||||
const { send, balance } = useChat();
|
||||
|
||||
// Блокируем отправку сообщений при нулевом или отрицательном балансе
|
||||
const disabled = (balance?.balance ?? 0) <= 0;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.inputWrapper}>
|
||||
<MessageInput onSend={send} />
|
||||
<MessageInput onSend={send} disabled={disabled} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -21,12 +21,14 @@ interface NewMessagesProps {
|
||||
chats: IChat[];
|
||||
isVisibleAll: boolean;
|
||||
maxHideVisibleCount?: number;
|
||||
currentBalance?: number;
|
||||
}
|
||||
|
||||
export default function NewMessages({
|
||||
chats,
|
||||
isVisibleAll = false,
|
||||
maxHideVisibleCount = 3,
|
||||
currentBalance = 0,
|
||||
}: NewMessagesProps) {
|
||||
const router = useRouter();
|
||||
const setCurrentChat = useChatStore(state => state.setCurrentChat);
|
||||
@ -60,7 +62,9 @@ export default function NewMessages({
|
||||
message: {
|
||||
type: chat.lastMessage.type,
|
||||
content: chat.lastMessage.text,
|
||||
sentWithZeroBalance: chat.lastMessage.sentWithZeroBalance,
|
||||
},
|
||||
currentBalance: currentBalance,
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Skeleton } from "@/components/ui";
|
||||
import { useBalance } from "@/hooks/balance/useBalance";
|
||||
import { useAppUiStore } from "@/providers/app-ui-store-provider";
|
||||
import { useChats } from "@/providers/chats-provider";
|
||||
|
||||
@ -11,6 +12,7 @@ import { ChatItemsList, NewMessages } from "..";
|
||||
export default function NewMessagesWrapper() {
|
||||
const t = useTranslations("Chat");
|
||||
const { unreadChats } = useChats();
|
||||
const { balance } = useBalance();
|
||||
|
||||
const { isVisibleAll } = useAppUiStore(state => state.chats.newMessages);
|
||||
const hasHydrated = useAppUiStore(state => state._hasHydrated);
|
||||
@ -32,7 +34,7 @@ export default function NewMessagesWrapper() {
|
||||
}}
|
||||
isVisibleViewAll={unreadChats.length > 1}
|
||||
>
|
||||
<NewMessages chats={unreadChats} isVisibleAll={isVisibleAll} />
|
||||
<NewMessages chats={unreadChats} isVisibleAll={isVisibleAll} currentBalance={balance} />
|
||||
</ChatItemsList>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { NewMessages, ViewAll } from "@/components/domains/chat";
|
||||
import { Skeleton } from "@/components/ui";
|
||||
import { useBalance } from "@/hooks/balance/useBalance";
|
||||
import { useAppUiStore } from "@/providers/app-ui-store-provider";
|
||||
import { useChats } from "@/providers/chats-provider";
|
||||
|
||||
@ -9,6 +10,7 @@ import styles from "./NewMessagesSection.module.scss";
|
||||
|
||||
export default function NewMessagesSection() {
|
||||
const { unreadChats } = useChats();
|
||||
const { balance } = useBalance();
|
||||
|
||||
const { isVisibleAll } = useAppUiStore(state => state.home.newMessages);
|
||||
const hasHydrated = useAppUiStore(state => state._hasHydrated);
|
||||
@ -29,7 +31,7 @@ export default function NewMessagesSection() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<NewMessages chats={unreadChats} isVisibleAll={isVisibleAll} />
|
||||
<NewMessages chats={unreadChats} isVisibleAll={isVisibleAll} currentBalance={balance} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -12,6 +12,7 @@ import styles from "./ModalSheet.module.scss";
|
||||
interface ModalSheetProps extends Omit<ModalProps, "isCloseButtonVisible"> {
|
||||
showCloseButton?: boolean;
|
||||
variant?: "white" | "gray";
|
||||
disableBackdropClose?: boolean;
|
||||
}
|
||||
|
||||
export default function ModalSheet({
|
||||
@ -22,6 +23,7 @@ export default function ModalSheet({
|
||||
modalClassName,
|
||||
showCloseButton = true,
|
||||
variant = "white",
|
||||
disableBackdropClose = false,
|
||||
ref,
|
||||
}: ModalSheetProps) {
|
||||
const [isOpen, setIsOpen] = useState(open);
|
||||
@ -37,7 +39,7 @@ export default function ModalSheet({
|
||||
return (
|
||||
<Modal
|
||||
open={open || isOpen}
|
||||
onClose={onClose}
|
||||
onClose={disableBackdropClose ? () => { /* Backdrop close disabled */ } : onClose}
|
||||
className={clsx(className, styles.overlay, {
|
||||
[styles.closed]: !open && isOpen,
|
||||
})}
|
||||
|
||||
@ -11,6 +11,7 @@ const ChatMessageSchema = z.object({
|
||||
text: z.string().optional(),
|
||||
isLast: z.boolean().optional(),
|
||||
suggestions: z.array(z.string()).optional(),
|
||||
sentWithZeroBalance: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const ChatSchema = z.object({
|
||||
|
||||
27
src/hooks/balance/useBalance.ts
Normal file
27
src/hooks/balance/useBalance.ts
Normal file
@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { useSocketEvent } from "@/hooks/socket/useSocketEvent";
|
||||
|
||||
import { useUserBalance } from "./useUserBalance";
|
||||
|
||||
export const useBalance = () => {
|
||||
const [socketBalance, setSocketBalance] = useState<number | null>(null);
|
||||
|
||||
// Получаем начальный баланс через API
|
||||
const { balance: apiBalance, isLoading } = useUserBalance();
|
||||
|
||||
// Слушаем обновления баланса через WebSocket
|
||||
useSocketEvent("balance_updated", (b) => {
|
||||
setSocketBalance(b.data.balance);
|
||||
});
|
||||
|
||||
// Приоритет: WebSocket > API > 0
|
||||
const currentBalance = socketBalance ?? apiBalance ?? 0;
|
||||
|
||||
return {
|
||||
balance: currentBalance,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
@ -113,6 +113,10 @@ export const useChatSocket = (
|
||||
emit("fetch_balance", { chatId });
|
||||
}, [emit, chatId]);
|
||||
|
||||
const clearRefillModals = useCallback(() => {
|
||||
setRefillModals(null);
|
||||
}, []);
|
||||
|
||||
// Auto top-up: silent flow (no UI prompt)
|
||||
const autoTopUpInProgressRef = useRef(false);
|
||||
// Timer for delayed unlock after error
|
||||
@ -352,6 +356,7 @@ export const useChatSocket = (
|
||||
read,
|
||||
startSession,
|
||||
endSession,
|
||||
clearRefillModals,
|
||||
|
||||
isLoadingSelfMessage,
|
||||
isLoadingAdvisorMessage,
|
||||
@ -380,6 +385,7 @@ export const useChatSocket = (
|
||||
read,
|
||||
startSession,
|
||||
endSession,
|
||||
clearRefillModals,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
14
src/hooks/refill/useShowRefillModal.ts
Normal file
14
src/hooks/refill/useShowRefillModal.ts
Normal file
@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useSocketEmit } from "@/services/socket";
|
||||
|
||||
export const useShowRefillModal = () => {
|
||||
const emit = useSocketEmit();
|
||||
|
||||
const showRefillModal = (chatId: string) => {
|
||||
// Запрашиваем у сервера показать модальное окно пополнения
|
||||
emit("request_refill_modal", { chatId });
|
||||
};
|
||||
|
||||
return { showRefillModal };
|
||||
};
|
||||
@ -42,6 +42,7 @@ export interface IRefillModals {
|
||||
};
|
||||
};
|
||||
products?: IRefillModalsProduct[];
|
||||
hasShownTimerModal?: boolean;
|
||||
}
|
||||
|
||||
export interface IAutoTopUpRequest {
|
||||
@ -63,6 +64,8 @@ export interface ClientToServerEvents {
|
||||
end_session: (data: { chatId: string }) => void;
|
||||
fetch_balance: (data: { chatId: string }) => void;
|
||||
deposit: (data: { amount: number }) => void;
|
||||
confirm_timer_modal_shown: () => void;
|
||||
request_refill_modal: (data: { chatId: string }) => void;
|
||||
}
|
||||
|
||||
export interface ServerToClientEventsBaseData<T> {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user