From c5002e1a7a3985088efc23fb937acf7e8b105958 Mon Sep 17 00:00:00 2001 From: "dev.daminik00" Date: Fri, 19 Sep 2025 00:11:23 +0200 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=B1=D0=BB=D1=8E=D1=80=20=D0=BD=D0=B0=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8=20=D0=B1=D0=BB?= =?UTF-8?q?=D0=BE=D0=BA=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D1=83=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20=D0=BD=D1=83=D0=BB=D0=B5=D0=B2=D0=BE=D0=BC=20=D0=B1?= =?UTF-8?q?=D0=B0=D0=BB=D0=B0=D0=BD=D1=81=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/CategoryChats/CategoryChats.tsx | 4 ++ .../chat/ChatCategories/ChatCategories.tsx | 3 ++ .../chat/ChatMessage/ChatMessage.module.scss | 1 + .../domains/chat/ChatMessage/ChatMessage.tsx | 30 ++++++++++++--- .../MessageText/MessageText.module.scss | 11 ++++++ .../ChatMessage/MessageText/MessageText.tsx | 36 +++++++++++++----- .../ChatModalsWrapper/ChatModalsWrapper.tsx | 38 ++++++++++++++++--- .../CorrespondenceStarted.tsx | 6 ++- .../CorrespondenceStartedWrapper.tsx | 3 ++ .../GlobalNewMessagesBanner.tsx | 4 +- .../LastMessagePreview.module.scss | 10 +++++ .../LastMessagePreview/LastMessagePreview.tsx | 10 ++++- .../chat/MessageInput/MessageInput.tsx | 7 ++-- .../MessageInputWrapper.tsx | 7 +++- .../domains/chat/NewMessages/NewMessages.tsx | 4 ++ .../NewMessagesWrapper/NewMessagesWrapper.tsx | 4 +- .../NewMessagesSection/NewMessagesSection.tsx | 4 +- src/components/ui/ModalSheet/ModalSheet.tsx | 4 +- src/entities/chats/types.ts | 1 + src/hooks/balance/useBalance.ts | 27 +++++++++++++ src/hooks/chats/useChatSocket.ts | 6 +++ src/hooks/refill/useShowRefillModal.ts | 14 +++++++ src/services/socket/events.ts | 3 ++ 23 files changed, 205 insertions(+), 32 deletions(-) create mode 100644 src/hooks/balance/useBalance.ts create mode 100644 src/hooks/refill/useShowRefillModal.ts diff --git a/src/components/domains/chat/CategoryChats/CategoryChats.tsx b/src/components/domains/chat/CategoryChats/CategoryChats.tsx index 0c5c30c..0c4b3c0 100644 --- a/src/components/domains/chat/CategoryChats/CategoryChats.tsx +++ b/src/components/domains/chat/CategoryChats/CategoryChats.tsx @@ -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 } diff --git a/src/components/domains/chat/ChatCategories/ChatCategories.tsx b/src/components/domains/chat/ChatCategories/ChatCategories.tsx index 8dadc63..08e5d3b 100644 --- a/src/components/domains/chat/ChatCategories/ChatCategories.tsx +++ b/src/components/domains/chat/ChatCategories/ChatCategories.tsx @@ -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("All"); const [maxVisibleChats, setMaxVisibleChats] = useState< @@ -59,6 +61,7 @@ export default function ChatCategories() { ))} diff --git a/src/components/domains/chat/ChatMessage/ChatMessage.module.scss b/src/components/domains/chat/ChatMessage/ChatMessage.module.scss index 303170d..04f7b78 100644 --- a/src/components/domains/chat/ChatMessage/ChatMessage.module.scss +++ b/src/components/domains/chat/ChatMessage/ChatMessage.module.scss @@ -10,4 +10,5 @@ align-self: flex-end; margin-left: auto; } + } diff --git a/src/components/domains/chat/ChatMessage/ChatMessage.tsx b/src/components/domains/chat/ChatMessage/ChatMessage.tsx index c6b0b51..566e4f5 100644 --- a/src/components/domains/chat/ChatMessage/ChatMessage.tsx +++ b/src/components/domains/chat/ChatMessage/ChatMessage.tsx @@ -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) {
{message.type === "text" && message.id !== "typing" && ( - + )} {message.id === "typing" && } diff --git a/src/components/domains/chat/ChatMessage/MessageText/MessageText.module.scss b/src/components/domains/chat/ChatMessage/MessageText/MessageText.module.scss index 92c4a0f..d22965d 100644 --- a/src/components/domains/chat/ChatMessage/MessageText/MessageText.module.scss +++ b/src/components/domains/chat/ChatMessage/MessageText/MessageText.module.scss @@ -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 + } } diff --git a/src/components/domains/chat/ChatMessage/MessageText/MessageText.tsx b/src/components/domains/chat/ChatMessage/MessageText/MessageText.tsx index e917056..ac0dcac 100644 --- a/src/components/domains/chat/ChatMessage/MessageText/MessageText.tsx +++ b/src/components/domains/chat/ChatMessage/MessageText/MessageText.tsx @@ -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 ( - - {text} - + + {text} + +
); } diff --git a/src/components/domains/chat/ChatModalsWrapper/ChatModalsWrapper.tsx b/src/components/domains/chat/ChatModalsWrapper/ChatModalsWrapper.tsx index 0d2852e..28fee1b 100644 --- a/src/components/domains/chat/ChatModalsWrapper/ChatModalsWrapper.tsx +++ b/src/components/domains/chat/ChatModalsWrapper/ChatModalsWrapper.tsx @@ -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 ( {modalChild === "refill-timer" && !!refillModals?.oneClick && ( { 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 } diff --git a/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx b/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx index b488bab..066f193 100644 --- a/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx +++ b/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx @@ -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() { )} diff --git a/src/components/domains/chat/GlobalNewMessagesBanner/GlobalNewMessagesBanner.tsx b/src/components/domains/chat/GlobalNewMessagesBanner/GlobalNewMessagesBanner.tsx index f4880ab..49d46f2 100644 --- a/src/components/domains/chat/GlobalNewMessagesBanner/GlobalNewMessagesBanner.tsx +++ b/src/components/domains/chat/GlobalNewMessagesBanner/GlobalNewMessagesBanner.tsx @@ -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 })} /> )} - + ); } diff --git a/src/components/domains/chat/LastMessagePreview/LastMessagePreview.module.scss b/src/components/domains/chat/LastMessagePreview/LastMessagePreview.module.scss index 42ddb38..7726e13 100644 --- a/src/components/domains/chat/LastMessagePreview/LastMessagePreview.module.scss +++ b/src/components/domains/chat/LastMessagePreview/LastMessagePreview.module.scss @@ -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; + } } diff --git a/src/components/domains/chat/LastMessagePreview/LastMessagePreview.tsx b/src/components/domains/chat/LastMessagePreview/LastMessagePreview.tsx index d3fd91c..5cab797 100644 --- a/src/components/domains/chat/LastMessagePreview/LastMessagePreview.tsx +++ b/src/components/domains/chat/LastMessagePreview/LastMessagePreview.tsx @@ -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()} diff --git a/src/components/domains/chat/MessageInput/MessageInput.tsx b/src/components/domains/chat/MessageInput/MessageInput.tsx index eac5336..a69d1b4 100644 --- a/src/components/domains/chat/MessageInput/MessageInput.tsx +++ b/src/components/domains/chat/MessageInput/MessageInput.tsx @@ -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) { />