AW-496-connect-chats

edits
This commit is contained in:
gofnnp 2025-07-21 22:03:27 +04:00
parent 324e7a9eaa
commit 5b8fc5047d
50 changed files with 724 additions and 198 deletions

View File

@ -294,6 +294,7 @@
},
"new_messages": "Новые сообщения",
"view_all": "View All ({count})",
"hide_all": "Hide",
"typing": "is typing...",
"voice_message": "Voice message",
"photo": "Photo",

View File

@ -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 (
<div className={styles.container}>
<ChatHeader />
<ChatHeader chatsPromise={loadChatsList()} />
<ChatMessagesWrapper />
<MessageInputWrapper />
<ChatModalsWrapper />

View File

@ -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 (
<div className={styles.container}>
<ChatListHeader />
<section className={styles.categories}>
<Suspense fallback={<NewMessagesWrapperSkeleton />}>
<NewMessagesWrapper chatsPromise={loadUnreadChats()} />
<NewMessagesWrapper chatsPromise={chatsPromise} />
</Suspense>
<Suspense fallback={<CorrespondenceStartedSkeleton />}>
<CorrespondenceStartedWrapper
chatsPromise={loadCorrespondenceStarted()}
/>
<CorrespondenceStartedWrapper chatsPromise={chatsPromise} />
</Suspense>
<Suspense fallback={<ChatCategoriesSkeleton />}>
<ChatCategories chatsPromise={loadCategorizedChats()} />
<ChatCategories chatsPromise={chatsPromise} />
</Suspense>
</section>
<NavigationBar />
<NavigationBar chatsPromise={chatsPromise} />
</div>
);
}

View File

@ -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 (
<Suspense fallback={<AdvisersSectionSkeleton />}>
<AdvisersSection promise={loadAssistants()} gridDisplayMode="vertical" />
<AdvisersSection
promiseAssistants={loadAssistants()}
promiseChats={loadChatsList()}
gridDisplayMode="vertical"
/>
</Suspense>
);
}

View File

@ -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 (
<DrawerProvider>
<Header className={styles.navBar} />
<main className={styles.main}>{children}</main>
<NavigationBar />
<ChatStoreProvider>
<Header className={styles.navBar} chatsPromise={chatsPromise} />
<main className={styles.main}>{children}</main>
<NavigationBar chatsPromise={chatsPromise} />
</ChatStoreProvider>
</DrawerProvider>
);
}

View File

@ -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 (
<section className={styles.page}>
<Suspense fallback={<NewMessagesSectionSkeleton />}>
<NewMessagesSection chatsPromise={chatsPromise} />
</Suspense>
<Horoscope />
<Suspense fallback={<AdvisersSectionSkeleton />}>
<AdvisersSection promise={loadAssistants()} />
<AdvisersSection
promiseAssistants={loadAssistants()}
promiseChats={chatsPromise}
/>
</Suspense>
<Suspense fallback={<CompatibilitySectionSkeleton />}>

View File

@ -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 (
<DrawerProvider>
<Header className={styles.navBar} />
<Header className={styles.navBar} chatsPromise={loadChatsList()} />
<main className={styles.main}>{children}</main>
</DrawerProvider>
);

View File

@ -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);

View File

@ -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({
<SocketProvider userId={userId}>
<RetainingStoreProvider>
<ChatsInitializationProvider>
<ToastProvider maxVisible={3}>{children}</ToastProvider>
<ToastProvider maxVisible={3}>
<AppUiStoreProvider>{children}</AppUiStoreProvider>
</ToastProvider>
</ChatsInitializationProvider>
</RetainingStoreProvider>
</SocketProvider>

View File

@ -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={

View File

@ -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<ICategorizedChats>;
chatsPromise: Promise<IGetChatsListResponse>;
}
export default function ChatCategories({ chatsPromise }: ChatCategoriesProps) {
const chats = use(chatsPromise);
const { categorizedChats } = useChatsSocket({ initialChats: chats });
const [activeChip, setActiveChip] = useState<string>("All");
const [maxVisibleChats, setMaxVisibleChats] = useState<
Partial<Record<string, number | null>>
>({});
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
}
>
<CategoryChats
chats={chats[key]}
maxVisibleChats={maxVisibleChats[key] ?? 3}
chats={categorizedChats[key]}
maxVisibleChats={maxVisibleChats[key] ?? MAX_HIDE_VISIBLE_COUNT}
/>
</ChatItemsList>
))}

View File

@ -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<IGetChatsListResponse>;
}
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 (
<header className={styles.header}>
<div className={styles.back} onClick={() => router.back()}>
<div className={styles.back} onClick={handleBack}>
<Icon
name={IconName.ChevronLeft}
size={{ height: 22, width: 22 }}
color="#374151"
/>
{/* <Badge className={styles.badge}>
<Typography weight="semiBold" size="xs" color="black">
2
</Typography>
</Badge> */}
{!!totalUnreadCount && (
<Badge className={styles.badge}>
<Typography weight="semiBold" size="xs" color="black">
{totalUnreadCount}
</Typography>
</Badge>
)}
</div>
<div className={styles.chatInfo}>
{!!currentChat?.assistantAvatar ? (
@ -60,7 +81,7 @@ export default function ChatHeader() {
<Typography weight="semiBold" className={styles.name}>
{currentChat?.assistantName}
<OnlineIndicator
isOnline={currentChat?.status === "inactive"}
isOnline={currentChat?.status === "active"}
className={styles.onlineIndicator}
/>
</Typography>

View File

@ -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 (
<div className={clsx(styles.chatItemsList, className)}>
<ChatItemsListHeader title={title} viewAllProps={viewAllProps} />
<ChatItemsListHeader
title={title}
viewAllProps={viewAllProps}
isVisibleViewAll={isVisibleViewAll}
/>
{children}
</div>
);

View File

@ -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 (
<div className={styles.chatItemsListHeader}>
<Typography className={styles.title} as="h3" size="lg" weight="bold">
{title}
</Typography>
<ViewAll {...viewAllProps} />
{isVisibleViewAll && <ViewAll {...viewAllProps} />}
</div>
);
}

View File

@ -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]);

View File

@ -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;
}

View File

@ -13,9 +13,6 @@ export default function ChatMessages({
}: ChatMessagesProps) {
return (
<div className={styles.container}>
{messages.map(message => (
<ChatMessage key={message.id} message={message} />
))}
{isLoadingAdvisorMessage && (
<ChatMessage
message={{
@ -28,6 +25,9 @@ export default function ChatMessages({
}}
/>
)}
{messages.map(message => (
<ChatMessage key={message.id} message={message} />
))}
</div>
);
}

View File

@ -7,5 +7,5 @@
.loaderTop {
display: flex;
justify-content: center;
padding: 8px 0;
padding-top: 16px;
}

View File

@ -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 (
<div
className={styles.messagesWrapper}
ref={messagesWrapperRef}
// onScroll={handleScroll}
onScroll={handleScroll}
>
{isLoadingOlder && hasMoreOlderMessages && (
<div className={styles.loaderTop}>

View File

@ -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;
}

View File

@ -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 (
<Card className={styles.container}>
<div className={styles.header}>
<Icon name={IconName.Pin} size={{ height: 17, width: 13.5 }} />
<Typography size="sm" color="muted">
{t("correspondence_started.pinned_chats")}
</Typography>
</div>
<div className={styles.chats}>
{chats.slice(0, maxVisibleChats).map(chat => (
<Container isVisibleAll={isVisibleAll} chats={chats}>
{chats
.slice(0, isVisibleAll ? chats.length : maxHideVisibleCount)
.map((chat, index) => (
<ChatItem
key={chat.id}
className={styles.chat}
style={
!isVisibleAll
? {
top: `${getTopPositionItem(index)}px`,
zIndex: 1111 - index,
position: !!index ? "absolute" : "relative",
}
: undefined
}
userAvatar={{
src: chat.assistantAvatar,
alt: chat.assistantName,
isOnline: true,
isOnline: chat.status === "active",
}}
name={chat.assistantName}
messagePreiew={
@ -62,7 +73,43 @@ export default function CorrespondenceStarted({
}}
/>
))}
</div>
</Card>
</Container>
);
}
function Container({
children,
isVisibleAll,
chats,
}: {
children: React.ReactNode;
isVisibleAll: boolean;
chats: IChat[];
}) {
const t = useTranslations("Chat");
if (isVisibleAll) {
return (
<Card className={styles.card}>
<div className={styles.header}>
<Icon name={IconName.Pin} size={{ height: 17, width: 13.5 }} />
<Typography size="sm" color="muted">
{t("correspondence_started.pinned_chats")}
</Typography>
</div>
<div className={styles.chats}>{children}</div>
</Card>
);
}
return (
<div
className={styles.container}
style={{
paddingBottom: getTopPositionItem(chats.length - 1),
}}
>
{children}
</div>
);
}

View File

@ -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<IChat[]>;
chatsPromise: Promise<IGetChatsListResponse>;
}
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<number | null>(null);
const { isVisibleAll } = useAppUiStore(
state => state.chats.correspondenceStarted
);
const hasHydrated = useAppUiStore(state => state._hasHydrated);
const setChatsCorrespondenceStarted = useAppUiStore(
state => state.setChatsCorrespondenceStarted
);
if (!hasHydrated) return <CorrespondenceStartedSkeleton />;
return (
<>
{!!chats.length && (
{!!startedChats.length && (
<ChatItemsList
title={t("correspondence_started.title")}
viewAllProps={{
count: chats.length,
count: startedChats.length,
isAll: isVisibleAll,
onClick: () => {
setMaxVisibleChats(prev => (prev ? null : chats.length));
setChatsCorrespondenceStarted({
isVisibleAll: !isVisibleAll,
});
},
}}
isVisibleViewAll={startedChats.length > 1}
>
<CorrespondenceStarted
chats={chats}
maxVisibleChats={maxVisibleChats ?? 3}
chats={startedChats}
isVisibleAll={isVisibleAll}
/>
</ChatItemsList>
)}

View File

@ -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;
}
}
}

View File

@ -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 (
<Container isVisibleAll={isVisibleAll} chats={chats}>
{chats
.slice(0, isVisibleAll ? chats.length : maxHideVisibleCount)
.map((chat, index) => (
<ChatItem
key={chat.id}
className={styles.newMessage}
style={
!isVisibleAll
? {
top: `${getTopPositionItem(index)}px`,
zIndex: 1111 - index,
position: !!index ? "absolute" : "relative",
}
: undefined
}
userAvatar={{
src: chat.assistantAvatar,
alt: chat.assistantName,
isOnline: chat.status === "active",
}}
name={chat.assistantName}
messagePreiew={
chat.lastMessage
? {
message: {
type: chat.lastMessage.type,
content: chat.lastMessage.text,
},
}
: null
}
time={formatTime(chat.updatedAt)}
badgeContent={chat.unreadCount}
onClick={() => {
setCurrentChat(chat);
router.push(ROUTES.chat(chat.assistantId));
}}
/>
))}
</Container>
);
}
function Container({
children,
isVisibleAll,
chats,
}: {
children: React.ReactNode;
isVisibleAll: boolean;
chats: IChat[];
}) {
if (isVisibleAll) {
return <Card className={styles.card}>{children}</Card>;
}
return (
<div
className={styles.container}
@ -34,43 +95,7 @@ export default function NewMessages({
paddingBottom: getTopPositionItem(chats.length - 1),
}}
>
{chats.map((chat, index) => (
<ChatItem
key={chat.id}
className={styles.newMessage}
style={
!isVisibleAll
? {
top: `${getTopPositionItem(index)}px`,
zIndex: 1111 - index,
position: !!index ? "absolute" : "relative",
}
: undefined
}
userAvatar={{
src: chat.assistantAvatar,
alt: chat.assistantName,
isOnline: true,
}}
name={chat.assistantName}
messagePreiew={
chat.lastMessage
? {
message: {
type: chat.lastMessage.type,
content: chat.lastMessage.text,
},
}
: null
}
time={formatTime(chat.updatedAt)}
badgeContent={chat.unreadCount}
onClick={() => {
setCurrentChat(chat);
router.push(ROUTES.chat(chat.assistantId));
}}
/>
))}
{children}
</div>
);
}

View File

@ -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<IChat[]>;
chatsPromise: Promise<IGetChatsListResponse>;
}
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<boolean>(false);
const { isVisibleAll } = useAppUiStore(state => state.chats.newMessages);
const hasHydrated = useAppUiStore(state => state._hasHydrated);
const setChatsNewMessages = useAppUiStore(state => state.setChatsNewMessages);
if (!hasHydrated) return <NewMessagesWrapperSkeleton />;
return (
<>
{!!chats.length && (
{!!unreadChats.length && (
<ChatItemsList
title={t("new_messages")}
viewAllProps={{
count: chats.length,
count: unreadChats.length,
isAll: isVisibleAll,
onClick: () => {
setIsVisibleAll(prev => !prev);
setChatsNewMessages({ isVisibleAll: !isVisibleAll });
},
}}
isVisibleViewAll={unreadChats.length > 1}
>
<NewMessages chats={chats} isVisibleAll={isVisibleAll} />
<NewMessages chats={unreadChats} isVisibleAll={isVisibleAll} />
</ChatItemsList>
)}
</>

View File

@ -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 (
<Button className={styles.viewAllButton} onClick={onClick}>
<Typography size="sm" weight="medium" color="muted">
{t("view_all", {
count,
})}
{isAll ? t("hide_all") : t("view_all", { count })}
</Typography>
</Button>
);

View File

@ -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 (
<Card
className={styles.card}
style={{ backgroundImage: `url(${photoUrl})` }}
onClick={() => {
if (chat) {
setCurrentChat(chat);
}
router.push(ROUTES.chat(_id));
}}
>
<div className={styles.content}>
<div className={styles.info}>

View File

@ -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<Assistant[]>;
promiseAssistants: Promise<Assistant[]>;
promiseChats: Promise<IGetChatsListResponse>;
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 => (
<AdviserCard key={adviser._id} {...adviser} />
<AdviserCard
key={adviser._id}
assistant={adviser}
chat={getChatByAssistantId(
adviser._id,
chats.categorizedChats[adviser.category]
)}
/>
))}
</Grid>
</Section>

View File

@ -0,0 +1,6 @@
.container {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 16px;
}

View File

@ -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<IGetChatsListResponse>;
}
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 <NewMessagesSectionSkeleton />;
return (
<>
{!!unreadChats.length && (
<div className={styles.container}>
{unreadChats.length > 1 && (
<ViewAll
count={unreadChats.length}
isAll={isVisibleAll}
onClick={() => {
setHomeNewMessages({ isVisibleAll: !isVisibleAll });
}}
/>
)}
<NewMessages chats={unreadChats} isVisibleAll={isVisibleAll} />
</div>
)}
</>
);
}
export const NewMessagesSectionSkeleton = () => {
return <Skeleton style={{ height: 100, width: "100%" }} />;
};

View File

@ -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,

View File

@ -1,7 +1,7 @@
.container {
width: 100%;
display: flex;
align-items: start;
align-items: flex-start;
gap: 4px;
}

View File

@ -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<IGetChatsListResponse>;
}
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 (
<header className={clsx(styles.header, className)}>
@ -27,7 +33,9 @@ export default function Header({ className }: HeaderProps) {
<Logo />
</Link>
<div>
<Icon name={IconName.Notification} />
<Link href={ROUTES.chat()}>
<Icon name={IconName.Notification} iconChildren={totalUnreadCount} />
</Link>
<Icon name={IconName.Search} />
</div>
</header>

View File

@ -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<IGetChatsListResponse>;
}
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 (
<nav className={styles.container}>
@ -25,6 +39,8 @@ export default function NavigationBar() {
? pathnameWithoutLocale === item.href
: pathnameWithoutLocale.startsWith(item.href);
const badge = getBadge(item, totalUnreadCount);
return (
<Link
key={item.key}
@ -32,10 +48,10 @@ export default function NavigationBar() {
className={clsx(styles.item, { [styles.active]: isActive })}
>
<Icon name={item.icon} color={isActive ? "#007AFF" : "#8A8D93"}>
{item.badge && (
{!!badge && (
<Badge className={styles.badge}>
<Typography weight="medium" size="xs" color="white">
{item.badge}
{badge}
</Typography>
</Badge>
)}

View File

@ -94,6 +94,7 @@ export type IconProps = {
cursor?: "pointer" | "auto";
iconStyle?: CSSProperties;
style?: CSSProperties;
iconChildren?: ReactNode;
};
export default function Icon({
@ -107,6 +108,7 @@ export default function Icon({
children,
cursor = "pointer",
style,
iconChildren,
...rest
}: IconProps) {
const Component = icons[name];
@ -132,7 +134,9 @@ export default function Icon({
display: "block",
...rest.iconStyle,
}}
/>
>
{iconChildren}
</Component>
{children}
</span>
);

View File

@ -26,10 +26,9 @@ export default function NotificationIcon(props: SVGProps<SVGSVGElement>) {
d="M14.75 0C19.1683 0 22.75 3.58172 22.75 8C22.75 12.4183 19.1683 16 14.75 16C10.3317 16 6.75 12.4183 6.75 8C6.75 3.58172 10.3317 0 14.75 0Z"
stroke="#E5E7EB"
/>
<path
d="M14.7766 12.0994C14.3079 12.0994 13.89 12.0189 13.5231 11.858C13.1585 11.697 12.8685 11.4732 12.6531 11.1868C12.44 10.898 12.324 10.563 12.305 10.1818H13.1999C13.2189 10.4162 13.2994 10.6186 13.4414 10.7891C13.5835 10.9571 13.7693 11.0874 13.9989 11.1797C14.2286 11.272 14.4831 11.3182 14.7624 11.3182C15.0749 11.3182 15.3519 11.2637 15.5934 11.1548C15.8349 11.0459 16.0243 10.8944 16.1616 10.7003C16.2989 10.5062 16.3675 10.2812 16.3675 10.0256C16.3675 9.75805 16.3013 9.52249 16.1687 9.31889C16.0361 9.11293 15.842 8.95194 15.5863 8.83594C15.3306 8.71993 15.0181 8.66193 14.6488 8.66193H14.0664V7.88068H14.6488C14.9376 7.88068 15.1909 7.8286 15.4087 7.72443C15.6289 7.62026 15.8005 7.47348 15.9237 7.28409C16.0491 7.0947 16.1119 6.87216 16.1119 6.61648C16.1119 6.37026 16.0574 6.15601 15.9485 5.97372C15.8396 5.79143 15.6857 5.64938 15.4869 5.54759C15.2904 5.44579 15.0584 5.39489 14.7908 5.39489C14.5399 5.39489 14.3031 5.44105 14.0806 5.53338C13.8604 5.62334 13.6805 5.75473 13.5408 5.92756C13.4012 6.09801 13.3254 6.30398 13.3136 6.54545H12.4613C12.4755 6.1643 12.5903 5.83049 12.8058 5.54403C13.0212 5.25521 13.3029 5.0303 13.6509 4.86932C14.0013 4.70833 14.386 4.62784 14.805 4.62784C15.2549 4.62784 15.6407 4.71899 15.9627 4.90128C16.2847 5.0812 16.5321 5.31913 16.7049 5.61506C16.8777 5.91098 16.9641 6.23059 16.9641 6.57386C16.9641 6.98343 16.8564 7.33262 16.641 7.62145C16.4279 7.91027 16.1379 8.11032 15.771 8.22159V8.27841C16.2302 8.35417 16.5889 8.54948 16.8469 8.86435C17.105 9.17685 17.234 9.56392 17.234 10.0256C17.234 10.4209 17.1263 10.776 16.9109 11.0909C16.6978 11.4034 16.4066 11.6496 16.0373 11.8295C15.668 12.0095 15.2478 12.0994 14.7766 12.0994Z"
fill="white"
/>
<text x="12" y="12" fill="white" fontSize="10">
{props.children}
</text>
<defs>
<clipPath id="clip0_20_1490">
<rect

View File

@ -1,9 +1,16 @@
.onlineIndicator {
aspect-ratio: 1/1;
border-radius: 50%;
background-color: #10b981;
border: 2px solid #fff;
&.online {
background-color: #10b981;
}
&.offline {
background-color: #9ca3af;
}
&.sm {
width: 8px;
}

View File

@ -15,10 +15,12 @@ export default function OnlineIndicator({
}: OnlineIndicatorProps) {
return (
<div
className={clsx(styles.onlineIndicator, styles[size], className)}
style={{
backgroundColor: isOnline ? "#10b981" : "#9CA3AF",
}}
className={clsx(
styles.onlineIndicator,
styles[size],
isOnline ? styles.online : styles.offline,
className
)}
/>
);
}

View File

@ -8,11 +8,12 @@
background: #f3f4f6;
font-size: 14px;
overflow-y: auto;
border: 1px solid transparent;
&:active,
&:focus,
&:focus-visible {
outline: 1px solid #191f29;
border: 1px solid #191f29;
}
&::placeholder {

View File

@ -4,6 +4,7 @@
align-items: center;
gap: 12px;
min-height: 94px;
width: 100%;
.content {
display: grid;

View File

@ -1,5 +1,7 @@
"use server";
import { revalidateTag } from "next/cache";
import { http } from "@/shared/api/httpClient";
import { API_ROUTES } from "@/shared/constants/api-routes";
import { ActionResponse } from "@/types";
@ -57,3 +59,7 @@ export async function fetchChatMessages(
return { data: null, error: errorMessage };
}
}
export async function revalidateChatsPage() {
revalidateTag("chats-list");
}

View File

@ -40,7 +40,7 @@ export const createChat = async (
export const getChatsList = async (): Promise<IGetChatsListResponse> => {
return http.get<IGetChatsListResponse>(API_ROUTES.getChatsList(), {
tags: ["chats", "list"],
tags: ["chats-list"],
schema: GetChatsListResponseSchema,
revalidate: 0,
});

View File

@ -1,18 +1,12 @@
import { cache } from "react";
import { createAllChats, getChatMessages, getChatsList } from "./api";
export const loadCreateAllChats = cache(createAllChats);
export const loadCreateAllChats = createAllChats;
export const loadChatsList = cache(getChatsList);
export const loadCategorizedChats = cache(() =>
loadChatsList().then(d => d.categorizedChats)
);
export const loadUnreadChats = cache(() =>
loadChatsList().then(d => d.unreadChats)
);
export const loadCorrespondenceStarted = cache(() =>
loadChatsList().then(d => d.startedChats)
);
export const loadChatsList = getChatsList;
export const loadCategorizedChats = () =>
loadChatsList().then(d => d.categorizedChats);
export const loadUnreadChats = () => loadChatsList().then(d => d.unreadChats);
export const loadCorrespondenceStarted = () =>
loadChatsList().then(d => d.startedChats);
export const loadChatMessages = cache(getChatMessages);
export const loadChatMessages = getChatMessages;

View File

@ -63,9 +63,7 @@ export const useChatSocket = (
const [refillModals, setRefillModals] = useState<IRefillModals | null>(null);
const isLoadingAdvisorMessage = useMemo(() => {
return (
messages.length > 0 && messages[messages.length - 1].role !== "assistant"
);
return messages.length > 0 && messages[0].role !== "assistant";
}, [messages]);
const joinChat = useCallback(
@ -86,7 +84,7 @@ export const useChatSocket = (
createdDate: new Date().toISOString(),
isRead: false,
};
setMessages(prev => [...prev, sendingMessage]);
setMessages(prev => [sendingMessage, ...prev]);
if (options.onNewMessage) {
options.onNewMessage(sendingMessage);
}
@ -145,8 +143,8 @@ export const useChatSocket = (
setMessages(prev => {
const ids = new Set(prev.map(m => m.id));
return [
...msgs.map(mapApiMessage).filter(m => !ids.has(m.id)),
...prev,
...msgs.map(mapApiMessage).filter(m => !ids.has(m.id)),
];
});
setPage(nextPage);
@ -182,7 +180,7 @@ export const useChatSocket = (
);
return Array.from(map.values()).sort(
(a, b) =>
new Date(a.createdDate).getTime() - new Date(b.createdDate).getTime()
new Date(b.createdDate).getTime() - new Date(a.createdDate).getTime()
);
});
@ -196,7 +194,7 @@ export const useChatSocket = (
});
useSocketEvent("session_started", s => setSession(s.data));
useSocketEvent("session_ended", () => setSession(null));
useSocketEvent("show_refill_modals", r => setRefillModals(r));
useSocketEvent("show_refill_modals", r => setRefillModals(r.data));
useEffect(() => {
if (!session?.maxFinishedAt) return;

View File

@ -0,0 +1,41 @@
"use client";
import { useMemo, useState } from "react";
import { IGetChatsListResponse } from "@/entities/chats/types";
import { useSocketEvent } from "../socket/useSocketEvent";
interface UseChatsSocketOptions {
initialChats?: IGetChatsListResponse;
}
export const useChatsSocket = (options: UseChatsSocketOptions = {}) => {
const initialChats = options.initialChats ?? {
categorizedChats: {},
startedChats: [],
unreadChats: [],
totalUnreadCount: 0,
};
const [chats, setChats] = useState<IGetChatsListResponse>(initialChats);
const [unreadCount, setUnreadCount] = useState<number>(
initialChats.totalUnreadCount
);
useSocketEvent("chats_updated", chats => setChats(chats));
useSocketEvent("unread_messages_count", count =>
setUnreadCount(count.unreadCount)
);
return useMemo(
() => ({
chats,
unreadChats: chats.unreadChats,
startedChats: chats.startedChats,
categorizedChats: chats.categorizedChats,
totalUnreadCount: unreadCount,
}),
[chats, unreadCount]
);
};

View File

@ -0,0 +1,39 @@
"use client";
import { createContext, type ReactNode, useContext, useRef } from "react";
import { useStore } from "zustand";
import { AppUiStore, createAppUiStore } from "@/stores/app-ui-store";
export type AppUiStoreApi = ReturnType<typeof createAppUiStore>;
export const AppUiStoreContext = createContext<AppUiStoreApi | undefined>(
undefined
);
export interface AppUiStoreProviderProps {
children: ReactNode;
}
export const AppUiStoreProvider = ({ children }: AppUiStoreProviderProps) => {
const storeRef = useRef<AppUiStoreApi | null>(null);
if (storeRef.current === null) {
storeRef.current = createAppUiStore();
}
return (
<AppUiStoreContext.Provider value={storeRef.current}>
{children}
</AppUiStoreContext.Provider>
);
};
export const useAppUiStore = <T,>(selector: (store: AppUiStore) => T): T => {
const appUiStoreContext = useContext(AppUiStoreContext);
if (!appUiStoreContext) {
throw new Error(`useAppUiStore must be used within AppUiStoreProvider`);
}
return useStore(appUiStoreContext, selector);
};

View File

@ -13,7 +13,7 @@ import { useChatSocket } from "@/hooks/chats/useChatSocket";
interface ChatContextValue extends ReturnType<typeof useChatSocket> {
messagesWrapperRef: React.RefObject<HTMLDivElement | null>;
scrollToBottom: () => void;
scrollToBottom: (behavior?: ScrollBehavior) => void;
}
const ChatContext = createContext<ChatContextValue | null>(null);
@ -47,11 +47,11 @@ export function ChatProvider({
const messagesWrapperRef = useRef<HTMLDivElement>(null);
const scrollToBottom = useCallback(() => {
const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
if (messagesWrapperRef.current) {
messagesWrapperRef.current.scrollTo({
top: messagesWrapperRef.current.scrollHeight,
behavior: "smooth",
behavior,
});
}
}, []);

View File

@ -1,3 +1,4 @@
import { IGetChatsListResponse } from "@/entities/chats/types";
import { Currency } from "@/types";
export interface IMessage {
@ -55,6 +56,10 @@ export interface IRefillModals {
products?: IRefillModalsProduct[];
}
export interface IUnreadMessagesCount {
unreadCount: number;
}
export interface ClientToServerEvents {
join_chat: (data: { chatId: string }) => void;
leave_chat: (data: { chatId: string }) => void;
@ -85,5 +90,9 @@ export interface ServerToClientEvents {
data: ServerToClientEventsBaseData<ISessionStarted>
) => void;
session_ended: (data: ServerToClientEventsBaseData<boolean>) => void;
show_refill_modals: (data: IRefillModals) => void;
show_refill_modals: (
data: ServerToClientEventsBaseData<IRefillModals>
) => void;
chats_updated: (data: IGetChatsListResponse) => void;
unread_messages_count: (data: IUnreadMessagesCount) => void;
}

View File

@ -2,12 +2,12 @@ import { IconName } from "@/components/ui";
import { ROUTES } from "./client-routes";
interface NavItem {
export interface NavItem {
key: string;
label: string;
icon: IconName;
href: string;
badge?: number;
badgeId?: string;
}
export const navItems: NavItem[] = [
@ -17,13 +17,13 @@ export const navItems: NavItem[] = [
icon: IconName.Home,
href: ROUTES.home(),
},
// {
// key: "chat",
// label: "Chat",
// icon: IconName.Chat,
// href: ROUTES.chat(),
// badge: 12,
// },
{
key: "chat",
label: "Chat",
icon: IconName.Chat,
href: ROUTES.chat(),
badgeId: "unreadCount",
},
{
key: "advisers",
label: "Advi...",

102
src/stores/app-ui-store.ts Normal file
View File

@ -0,0 +1,102 @@
"use client";
import { createStore } from "zustand";
import { persist } from "zustand/middleware";
interface AppUiState {
chats: {
correspondenceStarted: {
isVisibleAll: boolean;
};
newMessages: {
isVisibleAll: boolean;
};
};
home: {
newMessages: {
isVisibleAll: boolean;
};
};
_hasHydrated: boolean;
}
export type AppUiActions = {
setChats: (chats: AppUiState["chats"]) => void;
setChatsCorrespondenceStarted: (
correspondenceStarted: AppUiState["chats"]["correspondenceStarted"]
) => void;
setChatsNewMessages: (
newMessages: AppUiState["chats"]["newMessages"]
) => void;
setHomeNewMessages: (newMessages: AppUiState["home"]["newMessages"]) => void;
clearAppUiData: () => void;
setHasHydrated: (hasHydrated: boolean) => void;
};
export type AppUiStore = AppUiState & AppUiActions;
const initialState: AppUiState = {
chats: {
correspondenceStarted: {
isVisibleAll: true,
},
newMessages: {
isVisibleAll: false,
},
},
home: {
newMessages: {
isVisibleAll: false,
},
},
_hasHydrated: false,
};
export const createAppUiStore = (initState: AppUiState = initialState) => {
return createStore<AppUiStore>()(
persist(
(set, get) => ({
...initState,
setChats: (chats: AppUiState["chats"]) => set({ chats }),
setChatsCorrespondenceStarted: (
correspondenceStarted: AppUiState["chats"]["correspondenceStarted"]
) =>
set({
chats: {
...get().chats,
correspondenceStarted,
},
}),
setChatsNewMessages: (
newMessages: AppUiState["chats"]["newMessages"]
) =>
set({
chats: {
...get().chats,
newMessages,
},
}),
setHomeNewMessages: (newMessages: AppUiState["home"]["newMessages"]) =>
set({ home: { ...get().home, newMessages } }),
clearAppUiData: () => set(initialState),
setHasHydrated: (hasHydrated: boolean) =>
set({ _hasHydrated: hasHydrated }),
}),
{
name: "app-ui-storage",
onRehydrateStorage: () => state => {
// Вызывается после загрузки данных из localStorage
if (state) {
state.setHasHydrated(true);
}
},
}
)
);
};