From 5b8fc5047dbfb52d383ac880e73d505f19edc7d9 Mon Sep 17 00:00:00 2001 From: gofnnp Date: Mon, 21 Jul 2025 22:03:27 +0400 Subject: [PATCH] AW-496-connect-chats edits --- messages/en.json | 1 + .../(chat)/chat/[assistantId]/page.tsx | 3 +- src/app/[locale]/(chat)/chat/page.tsx | 22 ++-- src/app/[locale]/(core)/advisers/page.tsx | 7 +- src/app/[locale]/(core)/layout.tsx | 11 +- src/app/[locale]/(core)/page.tsx | 13 ++- src/app/[locale]/(payment)/layout.tsx | 3 +- src/app/[locale]/auth/callback/route.ts | 1 + src/app/[locale]/layout.tsx | 14 ++- .../chat/CategoryChats/CategoryChats.tsx | 2 +- .../chat/ChatCategories/ChatCategories.tsx | 27 +++-- .../domains/chat/ChatHeader/ChatHeader.tsx | 43 ++++++-- .../chat/ChatItemsList/ChatItemsList.tsx | 8 +- .../ChatItemsListHeader.tsx | 4 +- .../domains/chat/ChatMessage/ChatMessage.tsx | 7 +- .../ChatMessages/ChatMessages.module.scss | 3 +- .../chat/ChatMessages/ChatMessages.tsx | 6 +- .../ChatMessagesWrapper.module.scss | 2 +- .../ChatMessagesWrapper.tsx | 29 +++-- .../CorrespondenceStarted.module.scss | 11 +- .../CorrespondenceStarted.tsx | 77 ++++++++++--- .../CorrespondenceStartedWrapper.tsx | 33 ++++-- .../chat/NewMessages/NewMessages.module.scss | 17 ++- .../domains/chat/NewMessages/NewMessages.tsx | 99 ++++++++++------- .../NewMessagesWrapper/NewMessagesWrapper.tsx | 25 +++-- .../domains/chat/ViewAll/ViewAll.tsx | 7 +- .../cards/AdviserCard/AdviserCard.tsx | 30 ++++-- .../AdvisersSection/AdvisersSection.tsx | 23 +++- .../NewMessagesSection.module.scss | 6 ++ .../NewMessagesSection/NewMessagesSection.tsx | 51 +++++++++ .../domains/dashboard/sections/index.ts | 4 + .../TextWithEmoji/TextWithEmoji.module.scss | 2 +- src/components/layout/Header/Header.tsx | 12 ++- .../layout/NavigationBar/NavigationBar.tsx | 24 ++++- src/components/ui/Icon/Icon.tsx | 6 +- src/components/ui/Icon/icons/Notification.tsx | 7 +- .../OnlineIndicator.module.scss | 9 +- .../ui/OnlineIndicator/OnlineIndicator.tsx | 10 +- .../TextareaAutoResize.module.scss | 3 +- .../widgets/ChatItem/ChatItem.module.scss | 1 + src/entities/chats/actions.ts | 6 ++ src/entities/chats/api.ts | 2 +- src/entities/chats/loaders.ts | 22 ++-- src/hooks/chats/useChatSocket.ts | 12 +-- src/hooks/chats/useChatsSocket.ts | 41 +++++++ src/providers/app-ui-store-provider.tsx | 39 +++++++ src/providers/chat-provider.tsx | 6 +- src/services/socket/events.ts | 11 +- src/shared/constants/navigation.tsx | 18 ++-- src/stores/app-ui-store.ts | 102 ++++++++++++++++++ 50 files changed, 724 insertions(+), 198 deletions(-) create mode 100644 src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.module.scss create mode 100644 src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.tsx create mode 100644 src/hooks/chats/useChatsSocket.ts create mode 100644 src/providers/app-ui-store-provider.tsx create mode 100644 src/stores/app-ui-store.ts diff --git a/messages/en.json b/messages/en.json index 29c7641..fd9824d 100644 --- a/messages/en.json +++ b/messages/en.json @@ -294,6 +294,7 @@ }, "new_messages": "Новые сообщения", "view_all": "View All ({count})", + "hide_all": "Hide", "typing": "is typing...", "voice_message": "Voice message", "photo": "Photo", diff --git a/src/app/[locale]/(chat)/chat/[assistantId]/page.tsx b/src/app/[locale]/(chat)/chat/[assistantId]/page.tsx index eef4ad4..1313da7 100644 --- a/src/app/[locale]/(chat)/chat/[assistantId]/page.tsx +++ b/src/app/[locale]/(chat)/chat/[assistantId]/page.tsx @@ -4,13 +4,14 @@ import { ChatModalsWrapper, MessageInputWrapper, } from "@/components/domains/chat"; +import { loadChatsList } from "@/entities/chats/loaders"; import styles from "./page.module.scss"; export default function Chat() { return (
- + diff --git a/src/app/[locale]/(chat)/chat/page.tsx b/src/app/[locale]/(chat)/chat/page.tsx index b158fdd..1c19638 100644 --- a/src/app/[locale]/(chat)/chat/page.tsx +++ b/src/app/[locale]/(chat)/chat/page.tsx @@ -10,32 +10,32 @@ import { NewMessagesWrapperSkeleton, } from "@/components/domains/chat"; import { NavigationBar } from "@/components/layout"; -import { - loadCategorizedChats, - loadCorrespondenceStarted, - loadUnreadChats, -} from "@/entities/chats/loaders"; +import { loadChatsList } from "@/entities/chats/loaders"; import styles from "./page.module.scss"; +export const dynamic = "force-dynamic"; +export const revalidate = 0; +export const fetchCache = "force-no-store"; + export default function Chats() { + const chatsPromise = loadChatsList(); + return (
}> - + }> - + }> - +
- +
); } diff --git a/src/app/[locale]/(core)/advisers/page.tsx b/src/app/[locale]/(core)/advisers/page.tsx index 1da4cfc..809a079 100644 --- a/src/app/[locale]/(core)/advisers/page.tsx +++ b/src/app/[locale]/(core)/advisers/page.tsx @@ -4,12 +4,17 @@ import { AdvisersSection, AdvisersSectionSkeleton, } from "@/components/domains/dashboard"; +import { loadChatsList } from "@/entities/chats/loaders"; import { loadAssistants } from "@/entities/dashboard/loaders"; export default function Advisers() { return ( }> - + ); } diff --git a/src/app/[locale]/(core)/layout.tsx b/src/app/[locale]/(core)/layout.tsx index 1a066e9..654e78a 100644 --- a/src/app/[locale]/(core)/layout.tsx +++ b/src/app/[locale]/(core)/layout.tsx @@ -1,4 +1,6 @@ import { DrawerProvider, Header, NavigationBar } from "@/components/layout"; +import { loadChatsList } from "@/entities/chats/loaders"; +import { ChatStoreProvider } from "@/providers/chat-store-provider"; import styles from "./layout.module.scss"; @@ -7,11 +9,14 @@ export default function CoreLayout({ }: Readonly<{ children: React.ReactNode; }>) { + const chatsPromise = loadChatsList(); return ( -
-
{children}
- + +
+
{children}
+ + ); } diff --git a/src/app/[locale]/(core)/page.tsx b/src/app/[locale]/(core)/page.tsx index 98b6bd9..11cb9ab 100644 --- a/src/app/[locale]/(core)/page.tsx +++ b/src/app/[locale]/(core)/page.tsx @@ -7,10 +7,13 @@ import { CompatibilitySectionSkeleton, MeditationSection, MeditationSectionSkeleton, + NewMessagesSection, + NewMessagesSectionSkeleton, PalmSection, PalmSectionSkeleton, } from "@/components/domains/dashboard"; import { Horoscope } from "@/components/widgets"; +import { loadChatsList } from "@/entities/chats/loaders"; import { loadAssistants, loadCompatibility, @@ -21,12 +24,20 @@ import { import styles from "./page.module.scss"; export default function Home() { + const chatsPromise = loadChatsList(); return (
+ }> + + + }> - + }> diff --git a/src/app/[locale]/(payment)/layout.tsx b/src/app/[locale]/(payment)/layout.tsx index f8c99ba..a717e7d 100644 --- a/src/app/[locale]/(payment)/layout.tsx +++ b/src/app/[locale]/(payment)/layout.tsx @@ -1,4 +1,5 @@ import { DrawerProvider, Header } from "@/components/layout"; +import { loadChatsList } from "@/entities/chats/loaders"; import styles from "./layout.module.scss"; @@ -9,7 +10,7 @@ export default function CoreLayout({ }>) { return ( -
+
{children}
); diff --git a/src/app/[locale]/auth/callback/route.ts b/src/app/[locale]/auth/callback/route.ts index 39e4049..f9ed64b 100644 --- a/src/app/[locale]/auth/callback/route.ts +++ b/src/app/[locale]/auth/callback/route.ts @@ -74,6 +74,7 @@ export async function GET(req: NextRequest) { `${nextUrl || ROUTES.payment()}`, process.env.NEXT_PUBLIC_APP_URL || "" ); + if (productId) redirectUrl.searchParams.set("productId", productId); if (placementId) redirectUrl.searchParams.set("placementId", placementId); if (paywallId) redirectUrl.searchParams.set("paywallId", paywallId); diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index b416ecf..acf3d6f 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -2,7 +2,7 @@ import "@/styles/reset.css"; import "@/styles/globals.css"; import "react-circular-progressbar/dist/styles.css"; -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import { Inter } from "next/font/google"; import { notFound } from "next/navigation"; import { hasLocale, NextIntlClientProvider } from "next-intl"; @@ -11,6 +11,7 @@ import clsx from "clsx"; import { loadUser, loadUserId } from "@/entities/user/loaders"; import { routing } from "@/i18n/routing"; +import { AppUiStoreProvider } from "@/providers/app-ui-store-provider"; import { ChatsInitializationProvider } from "@/providers/chats-initialization-provider"; import { RetainingStoreProvider } from "@/providers/retaining-store-provider"; import SocketProvider from "@/providers/socket-provider"; @@ -29,6 +30,13 @@ const inter = Inter({ variable: "--font-inter", }); +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + maximumScale: 1, + userScalable: false, +}; + export const metadata: Metadata = { title: "WIT", description: @@ -60,7 +68,9 @@ export default async function RootLayout({ - {children} + + {children} + diff --git a/src/components/domains/chat/CategoryChats/CategoryChats.tsx b/src/components/domains/chat/CategoryChats/CategoryChats.tsx index 565d475..5215927 100644 --- a/src/components/domains/chat/CategoryChats/CategoryChats.tsx +++ b/src/components/domains/chat/CategoryChats/CategoryChats.tsx @@ -31,7 +31,7 @@ export default function CategoryChats({ userAvatar={{ src: chat.assistantAvatar, alt: chat.assistantName, - isOnline: true, + isOnline: chat.status === "active", }} name={chat.assistantName} messagePreiew={ diff --git a/src/components/domains/chat/ChatCategories/ChatCategories.tsx b/src/components/domains/chat/ChatCategories/ChatCategories.tsx index 7f851f9..33a3b13 100644 --- a/src/components/domains/chat/ChatCategories/ChatCategories.tsx +++ b/src/components/domains/chat/ChatCategories/ChatCategories.tsx @@ -4,31 +4,36 @@ import { use, useState } from "react"; import { Skeleton } from "@/components/ui"; import { Chips } from "@/components/widgets"; -import { ICategorizedChats } from "@/entities/chats/types"; +import { IGetChatsListResponse } from "@/entities/chats/types"; +import { useChatsSocket } from "@/hooks/chats/useChatsSocket"; import { CategoryChats, ChatItemsList } from ".."; +const MAX_HIDE_VISIBLE_COUNT = 3; + interface ChatCategoriesProps { - chatsPromise: Promise; + chatsPromise: Promise; } export default function ChatCategories({ chatsPromise }: ChatCategoriesProps) { const chats = use(chatsPromise); + const { categorizedChats } = useChatsSocket({ initialChats: chats }); + const [activeChip, setActiveChip] = useState("All"); const [maxVisibleChats, setMaxVisibleChats] = useState< Partial> >({}); - const chips = Object.keys(chats).map(key => ({ + const chips = Object.keys(categorizedChats).map(key => ({ text: key, })); chips.unshift({ text: "All", }); - const filteredChats = Object.keys(chats).filter(key => { + const filteredChats = Object.keys(categorizedChats).filter(key => { if (activeChip === "All") return true; - return chats[key].some(chat => chat.category === activeChip); + return categorizedChats[key].some(chat => chat.category === activeChip); }); return ( @@ -44,18 +49,22 @@ export default function ChatCategories({ chatsPromise }: ChatCategoriesProps) { title={key} key={key} viewAllProps={{ - count: chats[key].length, + count: categorizedChats[key].length, + isAll: !!maxVisibleChats[key], onClick: () => { setMaxVisibleChats(prev => ({ ...prev, - [key]: !!prev[key] ? null : chats[key].length, + [key]: !!prev[key] ? null : categorizedChats[key].length, })); }, }} + isVisibleViewAll={ + categorizedChats[key].length > MAX_HIDE_VISIBLE_COUNT + } > ))} diff --git a/src/components/domains/chat/ChatHeader/ChatHeader.tsx b/src/components/domains/chat/ChatHeader/ChatHeader.tsx index c32af4c..5a65eac 100644 --- a/src/components/domains/chat/ChatHeader/ChatHeader.tsx +++ b/src/components/domains/chat/ChatHeader/ChatHeader.tsx @@ -1,11 +1,20 @@ "use client"; -import { useEffect, useState } from "react"; +import { use, useEffect, useState } from "react"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; -import { Icon, IconName, OnlineIndicator, Typography } from "@/components/ui"; +import { + Badge, + Icon, + IconName, + OnlineIndicator, + Typography, +} from "@/components/ui"; +import { revalidateChatsPage } from "@/entities/chats/actions"; +import { IGetChatsListResponse } from "@/entities/chats/types"; +import { useChatsSocket } from "@/hooks/chats/useChatsSocket"; import { useChat } from "@/providers/chat-provider"; import { useChatStore } from "@/providers/chat-store-provider"; import { formatSecondsToHHMMSS } from "@/shared/utils/date"; @@ -13,12 +22,17 @@ import { delay } from "@/shared/utils/delay"; import styles from "./ChatHeader.module.scss"; -export default function ChatHeader() { +interface ChatHeaderProps { + chatsPromise: Promise; +} + +export default function ChatHeader({ chatsPromise }: ChatHeaderProps) { const t = useTranslations("Chat"); const router = useRouter(); const currentChat = useChatStore(state => state.currentChat); const { isLoadingAdvisorMessage, isAvailableChatting } = useChat(); - + const chats = use(chatsPromise); + const { totalUnreadCount } = useChatsSocket({ initialChats: chats }); const [timer, setTimer] = useState(0); useEffect(() => { @@ -30,19 +44,26 @@ export default function ChatHeader() { })(); }, [isAvailableChatting, timer]); + const handleBack = async () => { + await revalidateChatsPage(); + router.back(); + }; + return (
-
router.back()}> +
- {/* - - 2 - - */} + {!!totalUnreadCount && ( + + + {totalUnreadCount} + + + )}
{!!currentChat?.assistantAvatar ? ( @@ -60,7 +81,7 @@ export default function ChatHeader() { {currentChat?.assistantName} diff --git a/src/components/domains/chat/ChatItemsList/ChatItemsList.tsx b/src/components/domains/chat/ChatItemsList/ChatItemsList.tsx index cbbe6cd..2397bdf 100644 --- a/src/components/domains/chat/ChatItemsList/ChatItemsList.tsx +++ b/src/components/domains/chat/ChatItemsList/ChatItemsList.tsx @@ -11,6 +11,7 @@ interface ChatItemsListProps { children: React.ReactNode; title: string; viewAllProps: ViewAllProps; + isVisibleViewAll?: boolean; } export default function ChatItemsList({ @@ -18,10 +19,15 @@ export default function ChatItemsList({ children, title, viewAllProps, + isVisibleViewAll = true, }: ChatItemsListProps) { return (
- + {children}
); diff --git a/src/components/domains/chat/ChatItemsListHeader/ChatItemsListHeader.tsx b/src/components/domains/chat/ChatItemsListHeader/ChatItemsListHeader.tsx index 70ac682..d48c447 100644 --- a/src/components/domains/chat/ChatItemsListHeader/ChatItemsListHeader.tsx +++ b/src/components/domains/chat/ChatItemsListHeader/ChatItemsListHeader.tsx @@ -7,18 +7,20 @@ import styles from "./ChatItemsListHeader.module.scss"; interface ChatItemsListHeaderProps { title: string; viewAllProps: ViewAllProps; + isVisibleViewAll?: boolean; } export default function ChatItemsListHeader({ title, viewAllProps, + isVisibleViewAll = true, }: ChatItemsListHeaderProps) { return (
{title} - + {isVisibleViewAll && }
); } diff --git a/src/components/domains/chat/ChatMessage/ChatMessage.tsx b/src/components/domains/chat/ChatMessage/ChatMessage.tsx index d098422..70078c1 100644 --- a/src/components/domains/chat/ChatMessage/ChatMessage.tsx +++ b/src/components/domains/chat/ChatMessage/ChatMessage.tsx @@ -33,7 +33,12 @@ export default function ChatMessage({ message }: ChatMessageProps) { const { read } = useChat(); useEffect(() => { - if (!!message.id && !message.isRead) { + if ( + !!message.id && + !message.isRead && + message.id !== "typing" && + !message.id.startsWith("sending-message") + ) { read([message.id]); } }, [message.id, message.isRead, read]); diff --git a/src/components/domains/chat/ChatMessages/ChatMessages.module.scss b/src/components/domains/chat/ChatMessages/ChatMessages.module.scss index a7f7c67..49f9b38 100644 --- a/src/components/domains/chat/ChatMessages/ChatMessages.module.scss +++ b/src/components/domains/chat/ChatMessages/ChatMessages.module.scss @@ -5,6 +5,7 @@ padding: 36px 16px; display: flex; flex-direction: column; - justify-content: flex-end; + justify-content: flex-start; + flex-direction: column-reverse; gap: 8px; } diff --git a/src/components/domains/chat/ChatMessages/ChatMessages.tsx b/src/components/domains/chat/ChatMessages/ChatMessages.tsx index 2f2e357..3dc786f 100644 --- a/src/components/domains/chat/ChatMessages/ChatMessages.tsx +++ b/src/components/domains/chat/ChatMessages/ChatMessages.tsx @@ -13,9 +13,6 @@ export default function ChatMessages({ }: ChatMessagesProps) { return (
- {messages.map(message => ( - - ))} {isLoadingAdvisorMessage && ( )} + {messages.map(message => ( + + ))}
); } diff --git a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss index 25038c5..a2bfcf2 100644 --- a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss +++ b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss @@ -7,5 +7,5 @@ .loaderTop { display: flex; justify-content: center; - padding: 8px 0; + padding-top: 16px; } diff --git a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx index 7d89d1a..de552f3 100644 --- a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx +++ b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useRef } from "react"; import { Spinner } from "@/components/ui"; import { useChat } from "@/providers/chat-provider"; @@ -17,15 +17,20 @@ export default function ChatMessagesWrapper() { hasMoreOlderMessages, isLoadingOlder, messagesWrapperRef, + loadOlder, scrollToBottom, } = useChat(); - // const handleScroll = useCallback(() => { - // const el = messagesWrapperRef.current; - // if (!el) return; + const isInitialScrollDone = useRef(false); - // if (el.scrollTop < 120) loadOlder(); - // }, [loadOlder]); + const handleScroll = useCallback(() => { + const el = messagesWrapperRef.current; + if (!el || !isInitialScrollDone.current) return; + + if (el.scrollTop < 100) { + loadOlder(); + } + }, [loadOlder, messagesWrapperRef]); const mappedMessages = useMemo(() => { const msgs = socketMessages.map(m => ({ @@ -40,14 +45,20 @@ export default function ChatMessagesWrapper() { }, [socketMessages]); useEffect(() => { - scrollToBottom(); - }, [scrollToBottom]); + if (socketMessages.length > 0 && !isInitialScrollDone.current) { + scrollToBottom(); + const timeout = setTimeout(() => { + isInitialScrollDone.current = true; + }, 1000); + return () => clearTimeout(timeout); + } + }, [socketMessages.length, scrollToBottom]); return (
{isLoadingOlder && hasMoreOlderMessages && (
diff --git a/src/components/domains/chat/CorrespondenceStarted/CorrespondenceStarted.module.scss b/src/components/domains/chat/CorrespondenceStarted/CorrespondenceStarted.module.scss index be62616..cafa006 100644 --- a/src/components/domains/chat/CorrespondenceStarted/CorrespondenceStarted.module.scss +++ b/src/components/domains/chat/CorrespondenceStarted/CorrespondenceStarted.module.scss @@ -1,4 +1,4 @@ -.container.container { +.card.card { padding: 13px 0; } @@ -21,3 +21,12 @@ } } } + +.container.container { + position: relative; + width: 100%; + height: fit-content; + display: flex; + flex-direction: column; + gap: 8px; +} diff --git a/src/components/domains/chat/CorrespondenceStarted/CorrespondenceStarted.tsx b/src/components/domains/chat/CorrespondenceStarted/CorrespondenceStarted.tsx index cc0c295..7053a1f 100644 --- a/src/components/domains/chat/CorrespondenceStarted/CorrespondenceStarted.tsx +++ b/src/components/domains/chat/CorrespondenceStarted/CorrespondenceStarted.tsx @@ -12,36 +12,47 @@ import { formatTime } from "@/shared/utils/date"; import styles from "./CorrespondenceStarted.module.scss"; +const getTopPositionItem = (index: number) => { + return Array.from({ length: index }, (_, i) => i).reduce((acc, current) => { + return acc + 11 / 1.5 ** current; + }, 0); +}; + interface CorrespondenceStartedProps { chats: IChat[]; - maxVisibleChats?: number; + isVisibleAll?: boolean; + maxHideVisibleCount?: number; } export default function CorrespondenceStarted({ chats, - maxVisibleChats = 3, + isVisibleAll = false, + maxHideVisibleCount = 3, }: CorrespondenceStartedProps) { const router = useRouter(); - const t = useTranslations("Chat"); const setCurrentChat = useChatStore(state => state.setCurrentChat); return ( - -
- - - {t("correspondence_started.pinned_chats")} - -
-
- {chats.slice(0, maxVisibleChats).map(chat => ( + + {chats + .slice(0, isVisibleAll ? chats.length : maxHideVisibleCount) + .map((chat, index) => ( ))} -
-
+ + ); +} + +function Container({ + children, + isVisibleAll, + chats, +}: { + children: React.ReactNode; + isVisibleAll: boolean; + chats: IChat[]; +}) { + const t = useTranslations("Chat"); + + if (isVisibleAll) { + return ( + +
+ + + {t("correspondence_started.pinned_chats")} + +
+ +
{children}
+
+ ); + } + return ( +
+ {children} +
); } diff --git a/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx b/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx index 6c8b78e..658bfea 100644 --- a/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx +++ b/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx @@ -1,15 +1,17 @@ "use client"; -import { use, useState } from "react"; +import { use } from "react"; import { useTranslations } from "next-intl"; import { Skeleton } from "@/components/ui"; -import { IChat } from "@/entities/chats/types"; +import { IGetChatsListResponse } from "@/entities/chats/types"; +import { useChatsSocket } from "@/hooks/chats/useChatsSocket"; +import { useAppUiStore } from "@/providers/app-ui-store-provider"; import { ChatItemsList, CorrespondenceStarted } from ".."; interface CorrespondenceStartedWrapperProps { - chatsPromise: Promise; + chatsPromise: Promise; } export default function CorrespondenceStartedWrapper({ @@ -17,24 +19,37 @@ export default function CorrespondenceStartedWrapper({ }: CorrespondenceStartedWrapperProps) { const t = useTranslations("Chat"); const chats = use(chatsPromise); + const { startedChats } = useChatsSocket({ initialChats: chats }); - const [maxVisibleChats, setMaxVisibleChats] = useState(null); + const { isVisibleAll } = useAppUiStore( + state => state.chats.correspondenceStarted + ); + const hasHydrated = useAppUiStore(state => state._hasHydrated); + const setChatsCorrespondenceStarted = useAppUiStore( + state => state.setChatsCorrespondenceStarted + ); + + if (!hasHydrated) return ; return ( <> - {!!chats.length && ( + {!!startedChats.length && ( { - setMaxVisibleChats(prev => (prev ? null : chats.length)); + setChatsCorrespondenceStarted({ + isVisibleAll: !isVisibleAll, + }); }, }} + isVisibleViewAll={startedChats.length > 1} > )} diff --git a/src/components/domains/chat/NewMessages/NewMessages.module.scss b/src/components/domains/chat/NewMessages/NewMessages.module.scss index e88ff89..86995f0 100644 --- a/src/components/domains/chat/NewMessages/NewMessages.module.scss +++ b/src/components/domains/chat/NewMessages/NewMessages.module.scss @@ -1,4 +1,4 @@ -.container { +.container.container { position: relative; width: 100%; height: fit-content; @@ -11,3 +11,18 @@ left: 0; width: 100%; } + +.card.card { + padding: 13px 0; + + & > .newMessage { + background-color: transparent; + border: none; + box-shadow: none; + border-bottom: 1px solid #f3f4f6; + + &:last-child { + border-bottom: none; + } + } +} diff --git a/src/components/domains/chat/NewMessages/NewMessages.tsx b/src/components/domains/chat/NewMessages/NewMessages.tsx index 81e5d87..7997c2f 100644 --- a/src/components/domains/chat/NewMessages/NewMessages.tsx +++ b/src/components/domains/chat/NewMessages/NewMessages.tsx @@ -2,6 +2,7 @@ import { useRouter } from "next/navigation"; +import { Card } from "@/components/ui"; import { ChatItem } from "@/components/widgets"; import { IChat } from "@/entities/chats/types"; import { useChatStore } from "@/providers/chat-store-provider"; @@ -19,14 +20,74 @@ const getTopPositionItem = (index: number) => { interface NewMessagesProps { chats: IChat[]; isVisibleAll: boolean; + maxHideVisibleCount?: number; } export default function NewMessages({ chats, isVisibleAll = false, + maxHideVisibleCount = 3, }: NewMessagesProps) { const router = useRouter(); const setCurrentChat = useChatStore(state => state.setCurrentChat); + + return ( + + {chats + .slice(0, isVisibleAll ? chats.length : maxHideVisibleCount) + .map((chat, index) => ( + { + setCurrentChat(chat); + router.push(ROUTES.chat(chat.assistantId)); + }} + /> + ))} + + ); +} + +function Container({ + children, + isVisibleAll, + chats, +}: { + children: React.ReactNode; + isVisibleAll: boolean; + chats: IChat[]; +}) { + if (isVisibleAll) { + return {children}; + } return (
- {chats.map((chat, index) => ( - { - setCurrentChat(chat); - router.push(ROUTES.chat(chat.assistantId)); - }} - /> - ))} + {children}
); } diff --git a/src/components/domains/chat/NewMessagesWrapper/NewMessagesWrapper.tsx b/src/components/domains/chat/NewMessagesWrapper/NewMessagesWrapper.tsx index 660d0d2..18ab9b4 100644 --- a/src/components/domains/chat/NewMessagesWrapper/NewMessagesWrapper.tsx +++ b/src/components/domains/chat/NewMessagesWrapper/NewMessagesWrapper.tsx @@ -1,15 +1,17 @@ "use client"; -import { use, useState } from "react"; +import { use } from "react"; import { useTranslations } from "next-intl"; import { Skeleton } from "@/components/ui"; -import { IChat } from "@/entities/chats/types"; +import { IGetChatsListResponse } from "@/entities/chats/types"; +import { useChatsSocket } from "@/hooks/chats/useChatsSocket"; +import { useAppUiStore } from "@/providers/app-ui-store-provider"; import { ChatItemsList, NewMessages } from ".."; interface NewMessagesWrapperProps { - chatsPromise: Promise; + chatsPromise: Promise; } export default function NewMessagesWrapper({ @@ -17,22 +19,29 @@ export default function NewMessagesWrapper({ }: NewMessagesWrapperProps) { const t = useTranslations("Chat"); const chats = use(chatsPromise); + const { unreadChats } = useChatsSocket({ initialChats: chats }); - const [isVisibleAll, setIsVisibleAll] = useState(false); + const { isVisibleAll } = useAppUiStore(state => state.chats.newMessages); + const hasHydrated = useAppUiStore(state => state._hasHydrated); + const setChatsNewMessages = useAppUiStore(state => state.setChatsNewMessages); + + if (!hasHydrated) return ; return ( <> - {!!chats.length && ( + {!!unreadChats.length && ( { - setIsVisibleAll(prev => !prev); + setChatsNewMessages({ isVisibleAll: !isVisibleAll }); }, }} + isVisibleViewAll={unreadChats.length > 1} > - + )} diff --git a/src/components/domains/chat/ViewAll/ViewAll.tsx b/src/components/domains/chat/ViewAll/ViewAll.tsx index ac789ee..e844bbc 100644 --- a/src/components/domains/chat/ViewAll/ViewAll.tsx +++ b/src/components/domains/chat/ViewAll/ViewAll.tsx @@ -7,19 +7,18 @@ import { Button, Typography } from "@/components/ui"; import styles from "./ViewAll.module.scss"; export interface ViewAllProps { + isAll: boolean; count: number; onClick?: () => void; } -export default function ViewAll({ count, onClick }: ViewAllProps) { +export default function ViewAll({ count, isAll, onClick }: ViewAllProps) { const t = useTranslations("Chat"); return ( ); diff --git a/src/components/domains/dashboard/cards/AdviserCard/AdviserCard.tsx b/src/components/domains/dashboard/cards/AdviserCard/AdviserCard.tsx index b65da6d..45a0eb3 100644 --- a/src/components/domains/dashboard/cards/AdviserCard/AdviserCard.tsx +++ b/src/components/domains/dashboard/cards/AdviserCard/AdviserCard.tsx @@ -1,21 +1,35 @@ +"use client"; + +import { useRouter } from "next/navigation"; + import { Button, Card, Stars, Typography } from "@/components/ui"; +import { IChat } from "@/entities/chats/types"; import { Assistant } from "@/entities/dashboard/types"; +import { useChatStore } from "@/providers/chat-store-provider"; +import { ROUTES } from "@/shared/constants/client-routes"; import styles from "./AdviserCard.module.scss"; -type AdviserCardProps = Assistant; +type AdviserCardProps = { + assistant: Assistant; + chat: IChat | null; +}; + +export default function AdviserCard({ assistant, chat }: AdviserCardProps) { + const router = useRouter(); + const { _id, name, photoUrl, rating, reviewCount, description } = assistant; + const setCurrentChat = useChatStore(state => state.setCurrentChat); -export default function AdviserCard({ - name, - photoUrl, - rating, - reviewCount, - description, -}: AdviserCardProps) { return ( { + if (chat) { + setCurrentChat(chat); + } + router.push(ROUTES.chat(_id)); + }} >
diff --git a/src/components/domains/dashboard/sections/AdvisersSection/AdvisersSection.tsx b/src/components/domains/dashboard/sections/AdvisersSection/AdvisersSection.tsx index 75999e8..1f95f79 100644 --- a/src/components/domains/dashboard/sections/AdvisersSection/AdvisersSection.tsx +++ b/src/components/domains/dashboard/sections/AdvisersSection/AdvisersSection.tsx @@ -2,6 +2,7 @@ import { use } from "react"; import clsx from "clsx"; import { Grid, Section, Skeleton } from "@/components/ui"; +import { IChat, IGetChatsListResponse } from "@/entities/chats/types"; import { Assistant } from "@/entities/dashboard/types"; import { AdviserCard } from "../../cards"; @@ -9,15 +10,22 @@ import { AdviserCard } from "../../cards"; import styles from "./AdvisersSection.module.scss"; interface AdvisersSectionProps { - promise: Promise; + promiseAssistants: Promise; + promiseChats: Promise; gridDisplayMode?: "vertical" | "horizontal"; } +const getChatByAssistantId = (assistantId: string, chats: IChat[]) => { + return chats.find(chat => chat.assistantId === assistantId) || null; +}; + export default function AdvisersSection({ - promise, + promiseAssistants, + promiseChats, gridDisplayMode = "horizontal", }: AdvisersSectionProps) { - const assistants = use(promise); + const assistants = use(promiseAssistants); + const chats = use(promiseChats); const columns = Math.ceil(assistants?.length / 2); return ( @@ -27,7 +35,14 @@ export default function AdvisersSection({ className={clsx(styles.grid, styles[gridDisplayMode])} > {assistants.map(adviser => ( - + ))}
diff --git a/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.module.scss b/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.module.scss new file mode 100644 index 0000000..1d09984 --- /dev/null +++ b/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.module.scss @@ -0,0 +1,6 @@ +.container { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 16px; +} diff --git a/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.tsx b/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.tsx new file mode 100644 index 0000000..c5a31ca --- /dev/null +++ b/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { use } from "react"; + +import { NewMessages, ViewAll } from "@/components/domains/chat"; +import { Skeleton } from "@/components/ui"; +import { IGetChatsListResponse } from "@/entities/chats/types"; +import { useChatsSocket } from "@/hooks/chats/useChatsSocket"; +import { useAppUiStore } from "@/providers/app-ui-store-provider"; + +import styles from "./NewMessagesSection.module.scss"; + +interface NewMessagesSectionProps { + chatsPromise: Promise; +} + +export default function NewMessagesSection({ + chatsPromise, +}: NewMessagesSectionProps) { + const chats = use(chatsPromise); + const { unreadChats } = useChatsSocket({ initialChats: chats }); + + const { isVisibleAll } = useAppUiStore(state => state.home.newMessages); + const hasHydrated = useAppUiStore(state => state._hasHydrated); + const setHomeNewMessages = useAppUiStore(state => state.setHomeNewMessages); + + if (!hasHydrated) return ; + + return ( + <> + {!!unreadChats.length && ( +
+ {unreadChats.length > 1 && ( + { + setHomeNewMessages({ isVisibleAll: !isVisibleAll }); + }} + /> + )} + +
+ )} + + ); +} + +export const NewMessagesSectionSkeleton = () => { + return ; +}; diff --git a/src/components/domains/dashboard/sections/index.ts b/src/components/domains/dashboard/sections/index.ts index 6fa52de..f373901 100644 --- a/src/components/domains/dashboard/sections/index.ts +++ b/src/components/domains/dashboard/sections/index.ts @@ -10,6 +10,10 @@ export { default as MeditationSection, MeditationSectionSkeleton, } from "./MeditationSection/MeditationSection"; +export { + default as NewMessagesSection, + NewMessagesSectionSkeleton, +} from "./NewMessagesSection/NewMessagesSection"; export { default as PalmSection, PalmSectionSkeleton, diff --git a/src/components/domains/email-marketing/compatibility/v1/TextWithEmoji/TextWithEmoji.module.scss b/src/components/domains/email-marketing/compatibility/v1/TextWithEmoji/TextWithEmoji.module.scss index 50b99bb..d08b57f 100644 --- a/src/components/domains/email-marketing/compatibility/v1/TextWithEmoji/TextWithEmoji.module.scss +++ b/src/components/domains/email-marketing/compatibility/v1/TextWithEmoji/TextWithEmoji.module.scss @@ -1,7 +1,7 @@ .container { width: 100%; display: flex; - align-items: start; + align-items: flex-start; gap: 4px; } diff --git a/src/components/layout/Header/Header.tsx b/src/components/layout/Header/Header.tsx index 3c2a90c..25940c8 100644 --- a/src/components/layout/Header/Header.tsx +++ b/src/components/layout/Header/Header.tsx @@ -1,9 +1,12 @@ "use client"; +import { use } from "react"; import Link from "next/link"; import clsx from "clsx"; import { Button, Icon, IconName } from "@/components/ui"; +import { IGetChatsListResponse } from "@/entities/chats/types"; +import { useChatsSocket } from "@/hooks/chats/useChatsSocket"; import { ROUTES } from "@/shared/constants/client-routes"; import { useDrawer } from ".."; @@ -13,10 +16,13 @@ import styles from "./Header.module.scss"; interface HeaderProps { className?: string; + chatsPromise: Promise; } -export default function Header({ className }: HeaderProps) { +export default function Header({ className, chatsPromise }: HeaderProps) { const { open } = useDrawer(); + const chats = use(chatsPromise); + const { totalUnreadCount } = useChatsSocket({ initialChats: chats }); return (
@@ -27,7 +33,9 @@ export default function Header({ className }: HeaderProps) {
- + + +
diff --git a/src/components/layout/NavigationBar/NavigationBar.tsx b/src/components/layout/NavigationBar/NavigationBar.tsx index 1c7b986..f13a166 100644 --- a/src/components/layout/NavigationBar/NavigationBar.tsx +++ b/src/components/layout/NavigationBar/NavigationBar.tsx @@ -1,21 +1,35 @@ "use client"; +import { use } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useLocale } from "next-intl"; import clsx from "clsx"; import { Badge, Icon, Typography } from "@/components/ui"; +import { IGetChatsListResponse } from "@/entities/chats/types"; +import { useChatsSocket } from "@/hooks/chats/useChatsSocket"; import { ROUTES } from "@/shared/constants/client-routes"; -import { navItems } from "@/shared/constants/navigation"; +import { NavItem, navItems } from "@/shared/constants/navigation"; import { stripLocale } from "@/shared/utils/path"; import styles from "./NavigationBar.module.scss"; -export default function NavigationBar() { +const getBadge = (item: NavItem, totalUnreadCount: number) => { + if (item.badgeId === "unreadCount") return totalUnreadCount; + return null; +}; + +interface NavigationBarProps { + chatsPromise: Promise; +} + +export default function NavigationBar({ chatsPromise }: NavigationBarProps) { const pathname = usePathname(); const locale = useLocale(); const pathnameWithoutLocale = stripLocale(pathname, locale); + const chats = use(chatsPromise); + const { totalUnreadCount } = useChatsSocket({ initialChats: chats }); return (