From f821fea322c955c3358f0843d81253165a970b2c Mon Sep 17 00:00:00 2001 From: gofnnp Date: Tue, 29 Jul 2025 19:26:03 +0400 Subject: [PATCH 1/7] AW-496-chat-improvement virtualization & optimization --- package-lock.json | 42 +++- package.json | 1 + .../chat/[assistantId]/page.module.scss | 1 + .../chat/ChatMessage/ChatMessage.module.scss | 1 + .../domains/chat/ChatMessage/ChatMessage.tsx | 56 +++-- .../chat/ChatMessages/ChatMessages.tsx | 8 +- .../ChatMessagesWrapper.module.scss | 31 ++- .../ChatMessagesWrapper.tsx | 203 ++++++++++++------ src/entities/user/types.ts | 8 +- src/hooks/chats/useChatSocket.ts | 60 ++---- src/providers/chat-provider.tsx | 32 +-- 11 files changed, 268 insertions(+), 175 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2b8b666..4994245 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@lottiefiles/dotlottie-react": "^0.14.1", + "@tanstack/react-virtual": "^3.13.12", "client-only": "^0.0.1", "clsx": "^2.1.1", "idb": "^8.0.3", @@ -200,13 +201,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz", - "integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.0", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { @@ -214,9 +215,9 @@ } }, "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz", - "integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1308,6 +1309,33 @@ "tslib": "^2.8.0" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", + "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", + "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", diff --git a/package.json b/package.json index 4e635de..bef9539 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@lottiefiles/dotlottie-react": "^0.14.1", + "@tanstack/react-virtual": "^3.13.12", "client-only": "^0.0.1", "clsx": "^2.1.1", "idb": "^8.0.3", diff --git a/src/app/[locale]/(chat)/chat/[assistantId]/page.module.scss b/src/app/[locale]/(chat)/chat/[assistantId]/page.module.scss index d7b23e8..428e6e8 100644 --- a/src/app/[locale]/(chat)/chat/[assistantId]/page.module.scss +++ b/src/app/[locale]/(chat)/chat/[assistantId]/page.module.scss @@ -2,4 +2,5 @@ display: flex; flex-direction: column; height: 100dvh; + position: relative; } diff --git a/src/components/domains/chat/ChatMessage/ChatMessage.module.scss b/src/components/domains/chat/ChatMessage/ChatMessage.module.scss index 74ec592..303170d 100644 --- a/src/components/domains/chat/ChatMessage/ChatMessage.module.scss +++ b/src/components/domains/chat/ChatMessage/ChatMessage.module.scss @@ -8,5 +8,6 @@ &.own { align-items: flex-end; 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 fdac54c..c6b0b51 100644 --- a/src/components/domains/chat/ChatMessage/ChatMessage.tsx +++ b/src/components/domains/chat/ChatMessage/ChatMessage.tsx @@ -3,11 +3,11 @@ import { useEffect } from "react"; import clsx from "clsx"; +import { IChatMessage } from "@/entities/chats/types"; import { useChat } from "@/providers/chat-provider"; +import { formatTime } from "@/shared/utils/date"; -import MessageAudio from "./MessageAudio/MessageAudio"; import MessageBubble from "./MessageBubble/MessageBubble"; -import MessageImage from "./MessageImage/MessageImage"; import MessageMeta from "./MessageMeta/MessageMeta"; import MessageStatus from "./MessageStatus/MessageStatus"; import MessageText from "./MessageText/MessageText"; @@ -16,21 +16,23 @@ import MessageTyping from "./MessageTyping/MessageTyping"; import styles from "./ChatMessage.module.scss"; export interface ChatMessageProps { - message: { - id: string; - type: "text" | "image" | "audio" | "typing"; - content?: string; - imageUrl?: string; - audioUrl?: string; - duration?: number; - time: string | null; - isOwn: boolean; - isRead?: boolean; - }; + // message: { + // id: string; + // type: "text" | "image" | "voice" | "typing"; + // text?: string; + // imageUrl?: string; + // audioUrl?: string; + // duration?: number; + // time: string | null; + // isOwn: boolean; + // isRead?: boolean; + // }; + message: IChatMessage; } export default function ChatMessage({ message }: ChatMessageProps) { const { isConnected, read } = useChat(); + const isOwn = message.role === "user"; useEffect(() => { if ( @@ -45,37 +47,33 @@ export default function ChatMessage({ message }: ChatMessageProps) { }, [message.id, message.isRead, read, isConnected]); return ( -
- - {message.type === "text" && ( - +
+ + {message.type === "text" && message.id !== "typing" && ( + )} - {message.type === "typing" && } + {message.id === "typing" && } - {message.type === "image" && ( + {/* {message.type === "image" && ( <> - {message.content && ( - - )} + {message.text && } )} - {message.type === "audio" && ( + {message.type === "voice" && ( <> - {message.content && ( - - )} + {message.text && } - )} + )} */} - - {message.isOwn && } + + {isOwn && }
); diff --git a/src/components/domains/chat/ChatMessages/ChatMessages.tsx b/src/components/domains/chat/ChatMessages/ChatMessages.tsx index 3dc786f..5123ed7 100644 --- a/src/components/domains/chat/ChatMessages/ChatMessages.tsx +++ b/src/components/domains/chat/ChatMessages/ChatMessages.tsx @@ -17,11 +17,11 @@ export default function ChatMessages({ )} diff --git a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss index af59278..7c44795 100644 --- a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss +++ b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.module.scss @@ -1,18 +1,45 @@ .messagesWrapper { flex: 1 1 0%; overflow-y: auto; - scroll-behavior: smooth; transition: padding-bottom 0.3s ease-in-out; + position: relative; + transform: scaleY(-1); } .loaderTop { display: flex; justify-content: center; - padding-top: 16px; + padding-bottom: 16px; } .suggestions.suggestions { // position: sticky; // bottom: 0; padding: 0 16px 36px; + margin-bottom: 0; + transform: scaleY(-1); +} + +.scrollToBottomButton.scrollToBottomButton { + position: absolute; + right: 16px; + display: flex; + justify-content: center; + align-items: center; + z-index: 444; + padding: 8px; + width: fit-content; + background-color: #fff; + box-shadow: 0 4px 6px #00000017; + + & > .badge { + position: absolute; + top: -8px; + right: -8px; + background-color: #fbbf24; + min-width: 24px; + min-height: 24px; + max-width: 28px; + max-height: 28px; + } } diff --git a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx index 0de2f99..b5650c7 100644 --- a/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx +++ b/src/components/domains/chat/ChatMessagesWrapper/ChatMessagesWrapper.tsx @@ -1,93 +1,174 @@ "use client"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useVirtualizer } from "@tanstack/react-virtual"; -import { Spinner } from "@/components/ui"; +import { Button, Icon, IconName, Spinner } from "@/components/ui"; import { useChat } from "@/providers/chat-provider"; -import { useChatStore } from "@/providers/chat-store-provider"; -import { formatTime } from "@/shared/utils/date"; -import { ChatMessages, Suggestions } from ".."; +import { ChatMessage, Suggestions } from ".."; import styles from "./ChatMessagesWrapper.module.scss"; export default function ChatMessagesWrapper() { + const messagesWrapperRef = useRef(null); + + const [isScrolledUp, setIsScrolledUp] = useState(false); + const { messages: socketMessages, isLoadingAdvisorMessage, hasMoreOlderMessages, isLoadingOlder, - messagesWrapperRef, + // unreadMessagesCount, loadOlder, - scrollToBottom, send, } = useChat(); - const { _hasHydrated } = useChatStore(state => state); + const messages = useMemo(() => { + const msgs = [...socketMessages]; + if (isLoadingAdvisorMessage) { + msgs.unshift({ + id: "typing", + type: "text", + text: "…", + role: "assistant", + isRead: false, + createdDate: new Date().toISOString(), + }); + } + return msgs; + }, [isLoadingAdvisorMessage, socketMessages]); - const [isLoadOlder, setIsLoadOlder] = useState(false); + const virtualizer = useVirtualizer({ + enabled: messages.length > 0, + count: hasMoreOlderMessages ? messages.length + 1 : messages.length, + getScrollElement: () => messagesWrapperRef.current, + measureElement: el => el.getBoundingClientRect().height, + getItemKey: idx => messages[idx]?.id ?? idx, + estimateSize: _i => 100, + overscan: 5, + paddingStart: 36, + paddingEnd: 36, + gap: 8, + }); - const handleScroll = useCallback(() => { - const el = messagesWrapperRef.current; - if (!el) return; + const items = virtualizer.getVirtualItems(); - if (el.scrollTop < 100) { - setIsLoadOlder(true); + const scrollToBottom = useCallback(() => { + virtualizer.scrollToOffset(0); + }, [virtualizer]); + + useEffect(() => { + const handleScroll = (e: WheelEvent) => { + e.preventDefault(); + const currentTarget = e.currentTarget as HTMLElement; + + if (currentTarget) { + currentTarget.scrollTop -= e.deltaY; + } + }; + messagesWrapperRef.current?.addEventListener("wheel", handleScroll, { + passive: false, + }); + return () => { + // eslint-disable-next-line react-hooks/exhaustive-deps + messagesWrapperRef.current?.removeEventListener("wheel", handleScroll); + }; + }, []); + + useEffect(() => { + const [lastItem] = [...items].reverse(); + + if (!lastItem) { + return; + } + + if ( + lastItem.index >= messages.length - 1 && + hasMoreOlderMessages && + !isLoadingOlder + ) { loadOlder(); } - }, [loadOlder, messagesWrapperRef]); - - const mappedMessages = useMemo(() => { - const msgs = socketMessages.map(m => ({ - id: m.id, - type: "text" as const, - content: m.text, - isOwn: m.role === "user", - isRead: m.isRead, - time: formatTime(m.createdDate), - })); - return msgs; - }, [socketMessages]); + }, [hasMoreOlderMessages, loadOlder, messages.length, isLoadingOlder, items]); useEffect(() => { - if (isLoadOlder) { - setIsLoadOlder(false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [socketMessages]); + if (!messagesWrapperRef.current || messages.length === 0) return; - useEffect(() => { - if (socketMessages.length > 0 && _hasHydrated && !isLoadOlder) { - const timeout = setTimeout(() => { - scrollToBottom(); - }); - return () => clearTimeout(timeout); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [socketMessages.length, scrollToBottom, _hasHydrated]); + setIsScrolledUp((virtualizer.scrollOffset || 0) > 600); + }, [virtualizer.scrollOffset, messages.length, messagesWrapperRef]); return ( -
- {isLoadingOlder && hasMoreOlderMessages && ( -
- -
+ <> + {isScrolledUp && ( + )} - - { - send(suggestion); - }} - /> -
+
+ { + send(suggestion); + }} + /> +
+ {items.map(virtualRow => { + const message = messages[virtualRow.index]; + const isLoaderRow = virtualRow.index > messages.length - 1; + + return ( +
+ {!isLoaderRow && ( + + )} + {isLoaderRow && ( +
+ +
+ )} +
+ ); + })} +
+
+ ); } diff --git a/src/entities/user/types.ts b/src/entities/user/types.ts index 67f7555..64b7b74 100644 --- a/src/entities/user/types.ts +++ b/src/entities/user/types.ts @@ -30,10 +30,10 @@ const PartnerSchema = z address: z.string(), }) .optional(), - birthdate: z.string(), - gender: z.string(), - age: z.number(), - sign: z.string(), + birthdate: z.string().optional(), + gender: z.string().optional(), + age: z.number().optional(), + sign: z.string().optional(), }) .optional(); diff --git a/src/hooks/chats/useChatSocket.ts b/src/hooks/chats/useChatSocket.ts index e8d70de..3853bc9 100644 --- a/src/hooks/chats/useChatSocket.ts +++ b/src/hooks/chats/useChatSocket.ts @@ -20,15 +20,10 @@ import type { const PAGE_LIMIT = 50; -type UIMessage = Pick< - IChatMessage, - "id" | "role" | "text" | "createdDate" | "isRead" | "suggestions" | "isLast" ->; - interface UseChatSocketOptions { initialMessages?: IChatMessage[]; initialTotal?: number; - onNewMessage?: (message: UIMessage) => void; + onNewMessage?: (message: IChatMessage) => void; } export const useChatSocket = ( @@ -39,18 +34,8 @@ export const useChatSocket = ( const status = useSocketStatus(); const emit = useSocketEmit(); - const mapApiMessage = (m: IChatMessage): UIMessage => ({ - id: m.id, - role: m.role, - text: m.text, - createdDate: m.createdDate, - isRead: m.isRead, - suggestions: m.suggestions, - isLast: m.isLast, - }); - - const [messages, setMessages] = useState(() => - options.initialMessages ? options.initialMessages.map(mapApiMessage) : [] + const [messages, setMessages] = useState( + () => options.initialMessages || [] ); const [page, setPage] = useState(1); const [totalCount, _setTotalCount] = useState( @@ -60,7 +45,6 @@ export const useChatSocket = ( const [balance, setBalance] = useState(null); const [session, setSession] = useState(null); const [isLoadingSelfMessage, setIsLoadingSelfMessage] = useState(false); - // const [isLoadingAdvisorMessage, setIsLoadingAdvisorMessage] = useState(false); const [isSessionExpired, setIsSessionExpired] = useState(false); const [refillModals, setRefillModals] = useState(null); const { suggestions, setSuggestions } = useChatStore(state => state); @@ -73,6 +57,10 @@ export const useChatSocket = ( ); }, [messages]); + const unreadMessagesCount = useMemo(() => { + return messages.filter(m => !m.isRead && m.role === "assistant").length; + }, [messages]); + const joinChat = useCallback( () => emit("join_chat", { chatId }), [emit, chatId] @@ -84,12 +72,13 @@ export const useChatSocket = ( const send = useCallback( (text: string) => { - const sendingMessage = { + const sendingMessage: IChatMessage = { id: `sending-message-${Date.now()}`, role: "user", text, createdDate: new Date().toISOString(), isRead: false, + type: "text", }; setMessages(prev => [sendingMessage, ...prev]); if (options.onNewMessage) { @@ -97,14 +86,18 @@ export const useChatSocket = ( } setIsLoadingSelfMessage(true); - // setIsLoadingAdvisorMessage(true); emit("send_message", { chatId, message: text }); }, [options, emit, chatId] ); const read = useCallback( - (ids: string[]) => emit("read_message", { messages: ids }), + (ids: string[]) => { + emit("read_message", { messages: ids }); + // setMessages(prev => + // prev.map(m => (ids.includes(m.id) ? { ...m, isRead: true } : m)) + // ); + }, [emit] ); const startSession = useCallback( @@ -149,10 +142,7 @@ export const useChatSocket = ( const { messages: msgs } = data; setMessages(prev => { const ids = new Set(prev.map(m => m.id)); - return [ - ...prev, - ...msgs.map(mapApiMessage).filter(m => !ids.has(m.id)), - ]; + return [...prev, ...msgs.filter(m => !ids.has(m.id))]; }); setPage(nextPage); } catch (e) { @@ -167,26 +157,16 @@ export const useChatSocket = ( if (!data?.length) return; if (data[0].role === "user") setIsLoadingSelfMessage(false); - // if (data[0].role === "assistant") setIsLoadingAdvisorMessage(false); setMessages(prev => { - const map = new Map(); + const map = new Map(); prev .filter(m => !m.id.startsWith("sending-message-")) .forEach(m => map.set(m.id, m)); - data.forEach(d => - map.set(d.id, { - id: d.id, - role: d.role, - text: d.text, - createdDate: d.createdDate, - isRead: d.isRead, - suggestions: d.suggestions, - isLast: d.isLast, - }) - ); + data.forEach(d => map.set(d.id, d)); + return Array.from(map.values()).sort( (a, b) => new Date(b.createdDate).getTime() - new Date(a.createdDate).getTime() @@ -275,6 +255,7 @@ export const useChatSocket = ( session, refillModals, suggestions, + unreadMessagesCount, send, read, @@ -296,6 +277,7 @@ export const useChatSocket = ( session, refillModals, suggestions, + unreadMessagesCount, isLoadingSelfMessage, isLoadingAdvisorMessage, isAvailableChatting, diff --git a/src/providers/chat-provider.tsx b/src/providers/chat-provider.tsx index 7310fd8..e2e8130 100644 --- a/src/providers/chat-provider.tsx +++ b/src/providers/chat-provider.tsx @@ -1,20 +1,11 @@ "use client"; -import { - createContext, - ReactNode, - useCallback, - useContext, - useRef, -} from "react"; +import { createContext, ReactNode, useContext } from "react"; import type { IChatMessage } from "@/entities/chats/types"; import { useChatSocket } from "@/hooks/chats/useChatSocket"; -interface ChatContextValue extends ReturnType { - messagesWrapperRef: React.RefObject; - scrollToBottom: (behavior?: ScrollBehavior) => void; -} +type ChatContextValue = ReturnType; const ChatContext = createContext(null); @@ -44,22 +35,5 @@ export function ChatProvider({ initialTotal, }); - const messagesWrapperRef = useRef(null); - - const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => { - if (messagesWrapperRef.current) { - messagesWrapperRef.current.scrollTo({ - top: messagesWrapperRef.current.scrollHeight, - behavior, - }); - } - }, []); - - return ( - - {children} - - ); + return {children}; } From d1fe43463e3c13d2ff59ae66a8f408c56f92cfd6 Mon Sep 17 00:00:00 2001 From: gofnnp Date: Thu, 31 Jul 2025 13:57:17 +0400 Subject: [PATCH 2/7] notification-sound not working in safari --- public/audio/notification-new-message-1.mp3 | Bin 0 -> 68545 bytes .../(chat)/chat/[assistantId]/page.tsx | 3 +- src/app/[locale]/(chat)/chat/page.tsx | 11 ++-- src/app/[locale]/(core)/layout.tsx | 6 +- src/app/[locale]/(core)/page.tsx | 2 +- src/app/[locale]/(payment)/layout.tsx | 3 +- src/app/[locale]/layout.tsx | 11 +++- .../chat/ChatCategories/ChatCategories.tsx | 14 ++--- .../domains/chat/ChatHeader/ChatHeader.tsx | 14 ++--- .../CorrespondenceStartedWrapper.tsx | 15 +---- .../NewMessagesWrapper/NewMessagesWrapper.tsx | 15 +---- .../NewMessagesSection/NewMessagesSection.tsx | 16 +---- src/components/layout/Header/Header.tsx | 10 +-- .../layout/NavigationBar/NavigationBar.tsx | 13 +--- src/hooks/audio/useAudio.ts | 37 +++++++++++ src/hooks/chats/useChatsSocket.ts | 26 +++++++- src/providers/chats-provider.tsx | 34 ++++++++++ src/services/audio/index.ts | 58 ++++++++++++++++++ src/shared/constants/audio/index.ts | 9 +++ 19 files changed, 201 insertions(+), 96 deletions(-) create mode 100644 public/audio/notification-new-message-1.mp3 create mode 100644 src/hooks/audio/useAudio.ts create mode 100644 src/providers/chats-provider.tsx create mode 100644 src/services/audio/index.ts create mode 100644 src/shared/constants/audio/index.ts diff --git a/public/audio/notification-new-message-1.mp3 b/public/audio/notification-new-message-1.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..38605898501d8ab481392658872a00d19fbde784 GIT binary patch literal 68545 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 - + diff --git a/src/app/[locale]/(chat)/chat/page.tsx b/src/app/[locale]/(chat)/chat/page.tsx index 1c19638..6199b68 100644 --- a/src/app/[locale]/(chat)/chat/page.tsx +++ b/src/app/[locale]/(chat)/chat/page.tsx @@ -10,7 +10,6 @@ import { NewMessagesWrapperSkeleton, } from "@/components/domains/chat"; import { NavigationBar } from "@/components/layout"; -import { loadChatsList } from "@/entities/chats/loaders"; import styles from "./page.module.scss"; @@ -19,23 +18,21 @@ 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)/layout.tsx b/src/app/[locale]/(core)/layout.tsx index 654e78a..a2730b8 100644 --- a/src/app/[locale]/(core)/layout.tsx +++ b/src/app/[locale]/(core)/layout.tsx @@ -1,5 +1,4 @@ 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"; @@ -9,13 +8,12 @@ export default function CoreLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const chatsPromise = loadChatsList(); return ( -
+
{children}
- + ); diff --git a/src/app/[locale]/(core)/page.tsx b/src/app/[locale]/(core)/page.tsx index 11cb9ab..54a0512 100644 --- a/src/app/[locale]/(core)/page.tsx +++ b/src/app/[locale]/(core)/page.tsx @@ -28,7 +28,7 @@ export default function Home() { return (
}> - + diff --git a/src/app/[locale]/(payment)/layout.tsx b/src/app/[locale]/(payment)/layout.tsx index a717e7d..f8c99ba 100644 --- a/src/app/[locale]/(payment)/layout.tsx +++ b/src/app/[locale]/(payment)/layout.tsx @@ -1,5 +1,4 @@ import { DrawerProvider, Header } from "@/components/layout"; -import { loadChatsList } from "@/entities/chats/loaders"; import styles from "./layout.module.scss"; @@ -10,7 +9,7 @@ export default function CoreLayout({ }>) { return ( -
+
{children}
); diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 10550cf..361e368 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -10,10 +10,12 @@ import { getMessages } from "next-intl/server"; import clsx from "clsx"; import YandexMetrika from "@/components/analytics/YandexMetrika"; +import { loadChatsList } from "@/entities/chats/loaders"; 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 { ChatsProvider } from "@/providers/chats-provider"; import { RetainingStoreProvider } from "@/providers/retaining-store-provider"; import SocketProvider from "@/providers/socket-provider"; import { ToastProvider } from "@/providers/toast-provider"; @@ -60,6 +62,7 @@ export default async function RootLayout({ const user = await loadUser(); const userId = await loadUserId(); + const chats = await loadChatsList(); return ( @@ -70,9 +73,11 @@ export default async function RootLayout({ - - {children} - + + + {children} + + diff --git a/src/components/domains/chat/ChatCategories/ChatCategories.tsx b/src/components/domains/chat/ChatCategories/ChatCategories.tsx index 33a3b13..8dadc63 100644 --- a/src/components/domains/chat/ChatCategories/ChatCategories.tsx +++ b/src/components/domains/chat/ChatCategories/ChatCategories.tsx @@ -1,23 +1,17 @@ "use client"; -import { use, useState } from "react"; +import { useState } from "react"; import { Skeleton } from "@/components/ui"; import { Chips } from "@/components/widgets"; -import { IGetChatsListResponse } from "@/entities/chats/types"; -import { useChatsSocket } from "@/hooks/chats/useChatsSocket"; +import { useChats } from "@/providers/chats-provider"; import { CategoryChats, ChatItemsList } from ".."; const MAX_HIDE_VISIBLE_COUNT = 3; -interface ChatCategoriesProps { - chatsPromise: Promise; -} - -export default function ChatCategories({ chatsPromise }: ChatCategoriesProps) { - const chats = use(chatsPromise); - const { categorizedChats } = useChatsSocket({ initialChats: chats }); +export default function ChatCategories() { + const { categorizedChats } = useChats(); const [activeChip, setActiveChip] = useState("All"); const [maxVisibleChats, setMaxVisibleChats] = useState< diff --git a/src/components/domains/chat/ChatHeader/ChatHeader.tsx b/src/components/domains/chat/ChatHeader/ChatHeader.tsx index 4cb8074..f54befe 100644 --- a/src/components/domains/chat/ChatHeader/ChatHeader.tsx +++ b/src/components/domains/chat/ChatHeader/ChatHeader.tsx @@ -1,6 +1,6 @@ "use client"; -import { use, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; @@ -13,26 +13,20 @@ import { UserAvatar, } 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 { useChats } from "@/providers/chats-provider"; import { formatSecondsToHHMMSS } from "@/shared/utils/date"; import { delay } from "@/shared/utils/delay"; import styles from "./ChatHeader.module.scss"; -interface ChatHeaderProps { - chatsPromise: Promise; -} - -export default function ChatHeader({ chatsPromise }: ChatHeaderProps) { +export default function ChatHeader() { 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 { totalUnreadCount } = useChats(); const [timer, setTimer] = useState(0); useEffect(() => { diff --git a/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx b/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx index 658bfea..b488bab 100644 --- a/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx +++ b/src/components/domains/chat/CorrespondenceStartedWrapper/CorrespondenceStartedWrapper.tsx @@ -1,25 +1,16 @@ "use client"; -import { use } from "react"; import { useTranslations } from "next-intl"; 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 { useChats } from "@/providers/chats-provider"; import { ChatItemsList, CorrespondenceStarted } from ".."; -interface CorrespondenceStartedWrapperProps { - chatsPromise: Promise; -} - -export default function CorrespondenceStartedWrapper({ - chatsPromise, -}: CorrespondenceStartedWrapperProps) { +export default function CorrespondenceStartedWrapper() { const t = useTranslations("Chat"); - const chats = use(chatsPromise); - const { startedChats } = useChatsSocket({ initialChats: chats }); + const { startedChats } = useChats(); const { isVisibleAll } = useAppUiStore( state => state.chats.correspondenceStarted diff --git a/src/components/domains/chat/NewMessagesWrapper/NewMessagesWrapper.tsx b/src/components/domains/chat/NewMessagesWrapper/NewMessagesWrapper.tsx index 18ab9b4..282a007 100644 --- a/src/components/domains/chat/NewMessagesWrapper/NewMessagesWrapper.tsx +++ b/src/components/domains/chat/NewMessagesWrapper/NewMessagesWrapper.tsx @@ -1,25 +1,16 @@ "use client"; -import { use } from "react"; import { useTranslations } from "next-intl"; 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 { useChats } from "@/providers/chats-provider"; import { ChatItemsList, NewMessages } from ".."; -interface NewMessagesWrapperProps { - chatsPromise: Promise; -} - -export default function NewMessagesWrapper({ - chatsPromise, -}: NewMessagesWrapperProps) { +export default function NewMessagesWrapper() { const t = useTranslations("Chat"); - const chats = use(chatsPromise); - const { unreadChats } = useChatsSocket({ initialChats: chats }); + const { unreadChats } = useChats(); const { isVisibleAll } = useAppUiStore(state => state.chats.newMessages); const hasHydrated = useAppUiStore(state => state._hasHydrated); diff --git a/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.tsx b/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.tsx index c5a31ca..3b8d670 100644 --- a/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.tsx +++ b/src/components/domains/dashboard/sections/NewMessagesSection/NewMessagesSection.tsx @@ -1,24 +1,14 @@ "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 { useChats } from "@/providers/chats-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 }); +export default function NewMessagesSection() { + const { unreadChats } = useChats(); const { isVisibleAll } = useAppUiStore(state => state.home.newMessages); const hasHydrated = useAppUiStore(state => state._hasHydrated); diff --git a/src/components/layout/Header/Header.tsx b/src/components/layout/Header/Header.tsx index 1aa5253..5a10f1a 100644 --- a/src/components/layout/Header/Header.tsx +++ b/src/components/layout/Header/Header.tsx @@ -1,12 +1,10 @@ "use client"; -import { use } from "react"; import Link from "next/link"; import clsx from "clsx"; import { Badge, Button, Icon, IconName, Typography } from "@/components/ui"; -import { IGetChatsListResponse } from "@/entities/chats/types"; -import { useChatsSocket } from "@/hooks/chats/useChatsSocket"; +import { useChats } from "@/providers/chats-provider"; import { ROUTES } from "@/shared/constants/client-routes"; import { useDrawer } from ".."; @@ -16,13 +14,11 @@ import styles from "./Header.module.scss"; interface HeaderProps { className?: string; - chatsPromise: Promise; } -export default function Header({ className, chatsPromise }: HeaderProps) { +export default function Header({ className }: HeaderProps) { const { open } = useDrawer(); - const chats = use(chatsPromise); - const { totalUnreadCount } = useChatsSocket({ initialChats: chats }); + const { totalUnreadCount } = useChats(); return (
diff --git a/src/components/layout/NavigationBar/NavigationBar.tsx b/src/components/layout/NavigationBar/NavigationBar.tsx index eb3639d..45a0aee 100644 --- a/src/components/layout/NavigationBar/NavigationBar.tsx +++ b/src/components/layout/NavigationBar/NavigationBar.tsx @@ -1,14 +1,12 @@ "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 { useChats } from "@/providers/chats-provider"; import { ROUTES } from "@/shared/constants/client-routes"; import { NavItem, navItems } from "@/shared/constants/navigation"; import { stripLocale } from "@/shared/utils/path"; @@ -20,16 +18,11 @@ const getBadge = (item: NavItem, totalUnreadCount: number) => { return null; }; -interface NavigationBarProps { - chatsPromise: Promise; -} - -export default function NavigationBar({ chatsPromise }: NavigationBarProps) { +export default function NavigationBar() { const pathname = usePathname(); const locale = useLocale(); const pathnameWithoutLocale = stripLocale(pathname, locale); - const chats = use(chatsPromise); - const { totalUnreadCount } = useChatsSocket({ initialChats: chats }); + const { totalUnreadCount } = useChats(); return (