AW-496-connect-chats
edits
This commit is contained in:
parent
324e7a9eaa
commit
5b8fc5047d
@ -294,6 +294,7 @@
|
|||||||
},
|
},
|
||||||
"new_messages": "Новые сообщения",
|
"new_messages": "Новые сообщения",
|
||||||
"view_all": "View All ({count})",
|
"view_all": "View All ({count})",
|
||||||
|
"hide_all": "Hide",
|
||||||
"typing": "is typing...",
|
"typing": "is typing...",
|
||||||
"voice_message": "Voice message",
|
"voice_message": "Voice message",
|
||||||
"photo": "Photo",
|
"photo": "Photo",
|
||||||
|
|||||||
@ -4,13 +4,14 @@ import {
|
|||||||
ChatModalsWrapper,
|
ChatModalsWrapper,
|
||||||
MessageInputWrapper,
|
MessageInputWrapper,
|
||||||
} from "@/components/domains/chat";
|
} from "@/components/domains/chat";
|
||||||
|
import { loadChatsList } from "@/entities/chats/loaders";
|
||||||
|
|
||||||
import styles from "./page.module.scss";
|
import styles from "./page.module.scss";
|
||||||
|
|
||||||
export default function Chat() {
|
export default function Chat() {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<ChatHeader />
|
<ChatHeader chatsPromise={loadChatsList()} />
|
||||||
<ChatMessagesWrapper />
|
<ChatMessagesWrapper />
|
||||||
<MessageInputWrapper />
|
<MessageInputWrapper />
|
||||||
<ChatModalsWrapper />
|
<ChatModalsWrapper />
|
||||||
|
|||||||
@ -10,32 +10,32 @@ import {
|
|||||||
NewMessagesWrapperSkeleton,
|
NewMessagesWrapperSkeleton,
|
||||||
} from "@/components/domains/chat";
|
} from "@/components/domains/chat";
|
||||||
import { NavigationBar } from "@/components/layout";
|
import { NavigationBar } from "@/components/layout";
|
||||||
import {
|
import { loadChatsList } from "@/entities/chats/loaders";
|
||||||
loadCategorizedChats,
|
|
||||||
loadCorrespondenceStarted,
|
|
||||||
loadUnreadChats,
|
|
||||||
} from "@/entities/chats/loaders";
|
|
||||||
|
|
||||||
import styles from "./page.module.scss";
|
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() {
|
export default function Chats() {
|
||||||
|
const chatsPromise = loadChatsList();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<ChatListHeader />
|
<ChatListHeader />
|
||||||
<section className={styles.categories}>
|
<section className={styles.categories}>
|
||||||
<Suspense fallback={<NewMessagesWrapperSkeleton />}>
|
<Suspense fallback={<NewMessagesWrapperSkeleton />}>
|
||||||
<NewMessagesWrapper chatsPromise={loadUnreadChats()} />
|
<NewMessagesWrapper chatsPromise={chatsPromise} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Suspense fallback={<CorrespondenceStartedSkeleton />}>
|
<Suspense fallback={<CorrespondenceStartedSkeleton />}>
|
||||||
<CorrespondenceStartedWrapper
|
<CorrespondenceStartedWrapper chatsPromise={chatsPromise} />
|
||||||
chatsPromise={loadCorrespondenceStarted()}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Suspense fallback={<ChatCategoriesSkeleton />}>
|
<Suspense fallback={<ChatCategoriesSkeleton />}>
|
||||||
<ChatCategories chatsPromise={loadCategorizedChats()} />
|
<ChatCategories chatsPromise={chatsPromise} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</section>
|
</section>
|
||||||
<NavigationBar />
|
<NavigationBar chatsPromise={chatsPromise} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,12 +4,17 @@ import {
|
|||||||
AdvisersSection,
|
AdvisersSection,
|
||||||
AdvisersSectionSkeleton,
|
AdvisersSectionSkeleton,
|
||||||
} from "@/components/domains/dashboard";
|
} from "@/components/domains/dashboard";
|
||||||
|
import { loadChatsList } from "@/entities/chats/loaders";
|
||||||
import { loadAssistants } from "@/entities/dashboard/loaders";
|
import { loadAssistants } from "@/entities/dashboard/loaders";
|
||||||
|
|
||||||
export default function Advisers() {
|
export default function Advisers() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<AdvisersSectionSkeleton />}>
|
<Suspense fallback={<AdvisersSectionSkeleton />}>
|
||||||
<AdvisersSection promise={loadAssistants()} gridDisplayMode="vertical" />
|
<AdvisersSection
|
||||||
|
promiseAssistants={loadAssistants()}
|
||||||
|
promiseChats={loadChatsList()}
|
||||||
|
gridDisplayMode="vertical"
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { DrawerProvider, Header, NavigationBar } from "@/components/layout";
|
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";
|
import styles from "./layout.module.scss";
|
||||||
|
|
||||||
@ -7,11 +9,14 @@ export default function CoreLayout({
|
|||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
const chatsPromise = loadChatsList();
|
||||||
return (
|
return (
|
||||||
<DrawerProvider>
|
<DrawerProvider>
|
||||||
<Header className={styles.navBar} />
|
<ChatStoreProvider>
|
||||||
<main className={styles.main}>{children}</main>
|
<Header className={styles.navBar} chatsPromise={chatsPromise} />
|
||||||
<NavigationBar />
|
<main className={styles.main}>{children}</main>
|
||||||
|
<NavigationBar chatsPromise={chatsPromise} />
|
||||||
|
</ChatStoreProvider>
|
||||||
</DrawerProvider>
|
</DrawerProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,10 +7,13 @@ import {
|
|||||||
CompatibilitySectionSkeleton,
|
CompatibilitySectionSkeleton,
|
||||||
MeditationSection,
|
MeditationSection,
|
||||||
MeditationSectionSkeleton,
|
MeditationSectionSkeleton,
|
||||||
|
NewMessagesSection,
|
||||||
|
NewMessagesSectionSkeleton,
|
||||||
PalmSection,
|
PalmSection,
|
||||||
PalmSectionSkeleton,
|
PalmSectionSkeleton,
|
||||||
} from "@/components/domains/dashboard";
|
} from "@/components/domains/dashboard";
|
||||||
import { Horoscope } from "@/components/widgets";
|
import { Horoscope } from "@/components/widgets";
|
||||||
|
import { loadChatsList } from "@/entities/chats/loaders";
|
||||||
import {
|
import {
|
||||||
loadAssistants,
|
loadAssistants,
|
||||||
loadCompatibility,
|
loadCompatibility,
|
||||||
@ -21,12 +24,20 @@ import {
|
|||||||
import styles from "./page.module.scss";
|
import styles from "./page.module.scss";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const chatsPromise = loadChatsList();
|
||||||
return (
|
return (
|
||||||
<section className={styles.page}>
|
<section className={styles.page}>
|
||||||
|
<Suspense fallback={<NewMessagesSectionSkeleton />}>
|
||||||
|
<NewMessagesSection chatsPromise={chatsPromise} />
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
<Horoscope />
|
<Horoscope />
|
||||||
|
|
||||||
<Suspense fallback={<AdvisersSectionSkeleton />}>
|
<Suspense fallback={<AdvisersSectionSkeleton />}>
|
||||||
<AdvisersSection promise={loadAssistants()} />
|
<AdvisersSection
|
||||||
|
promiseAssistants={loadAssistants()}
|
||||||
|
promiseChats={chatsPromise}
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
<Suspense fallback={<CompatibilitySectionSkeleton />}>
|
<Suspense fallback={<CompatibilitySectionSkeleton />}>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { DrawerProvider, Header } from "@/components/layout";
|
import { DrawerProvider, Header } from "@/components/layout";
|
||||||
|
import { loadChatsList } from "@/entities/chats/loaders";
|
||||||
|
|
||||||
import styles from "./layout.module.scss";
|
import styles from "./layout.module.scss";
|
||||||
|
|
||||||
@ -9,7 +10,7 @@ export default function CoreLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<DrawerProvider>
|
<DrawerProvider>
|
||||||
<Header className={styles.navBar} />
|
<Header className={styles.navBar} chatsPromise={loadChatsList()} />
|
||||||
<main className={styles.main}>{children}</main>
|
<main className={styles.main}>{children}</main>
|
||||||
</DrawerProvider>
|
</DrawerProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -74,6 +74,7 @@ export async function GET(req: NextRequest) {
|
|||||||
`${nextUrl || ROUTES.payment()}`,
|
`${nextUrl || ROUTES.payment()}`,
|
||||||
process.env.NEXT_PUBLIC_APP_URL || ""
|
process.env.NEXT_PUBLIC_APP_URL || ""
|
||||||
);
|
);
|
||||||
|
|
||||||
if (productId) redirectUrl.searchParams.set("productId", productId);
|
if (productId) redirectUrl.searchParams.set("productId", productId);
|
||||||
if (placementId) redirectUrl.searchParams.set("placementId", placementId);
|
if (placementId) redirectUrl.searchParams.set("placementId", placementId);
|
||||||
if (paywallId) redirectUrl.searchParams.set("paywallId", paywallId);
|
if (paywallId) redirectUrl.searchParams.set("paywallId", paywallId);
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import "@/styles/reset.css";
|
|||||||
import "@/styles/globals.css";
|
import "@/styles/globals.css";
|
||||||
import "react-circular-progressbar/dist/styles.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 { Inter } from "next/font/google";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { hasLocale, NextIntlClientProvider } from "next-intl";
|
import { hasLocale, NextIntlClientProvider } from "next-intl";
|
||||||
@ -11,6 +11,7 @@ import clsx from "clsx";
|
|||||||
|
|
||||||
import { loadUser, loadUserId } from "@/entities/user/loaders";
|
import { loadUser, loadUserId } from "@/entities/user/loaders";
|
||||||
import { routing } from "@/i18n/routing";
|
import { routing } from "@/i18n/routing";
|
||||||
|
import { AppUiStoreProvider } from "@/providers/app-ui-store-provider";
|
||||||
import { ChatsInitializationProvider } from "@/providers/chats-initialization-provider";
|
import { ChatsInitializationProvider } from "@/providers/chats-initialization-provider";
|
||||||
import { RetainingStoreProvider } from "@/providers/retaining-store-provider";
|
import { RetainingStoreProvider } from "@/providers/retaining-store-provider";
|
||||||
import SocketProvider from "@/providers/socket-provider";
|
import SocketProvider from "@/providers/socket-provider";
|
||||||
@ -29,6 +30,13 @@ const inter = Inter({
|
|||||||
variable: "--font-inter",
|
variable: "--font-inter",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const viewport: Viewport = {
|
||||||
|
width: "device-width",
|
||||||
|
initialScale: 1,
|
||||||
|
maximumScale: 1,
|
||||||
|
userScalable: false,
|
||||||
|
};
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "WIT",
|
title: "WIT",
|
||||||
description:
|
description:
|
||||||
@ -60,7 +68,9 @@ export default async function RootLayout({
|
|||||||
<SocketProvider userId={userId}>
|
<SocketProvider userId={userId}>
|
||||||
<RetainingStoreProvider>
|
<RetainingStoreProvider>
|
||||||
<ChatsInitializationProvider>
|
<ChatsInitializationProvider>
|
||||||
<ToastProvider maxVisible={3}>{children}</ToastProvider>
|
<ToastProvider maxVisible={3}>
|
||||||
|
<AppUiStoreProvider>{children}</AppUiStoreProvider>
|
||||||
|
</ToastProvider>
|
||||||
</ChatsInitializationProvider>
|
</ChatsInitializationProvider>
|
||||||
</RetainingStoreProvider>
|
</RetainingStoreProvider>
|
||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export default function CategoryChats({
|
|||||||
userAvatar={{
|
userAvatar={{
|
||||||
src: chat.assistantAvatar,
|
src: chat.assistantAvatar,
|
||||||
alt: chat.assistantName,
|
alt: chat.assistantName,
|
||||||
isOnline: true,
|
isOnline: chat.status === "active",
|
||||||
}}
|
}}
|
||||||
name={chat.assistantName}
|
name={chat.assistantName}
|
||||||
messagePreiew={
|
messagePreiew={
|
||||||
|
|||||||
@ -4,31 +4,36 @@ import { use, useState } from "react";
|
|||||||
|
|
||||||
import { Skeleton } from "@/components/ui";
|
import { Skeleton } from "@/components/ui";
|
||||||
import { Chips } from "@/components/widgets";
|
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 "..";
|
import { CategoryChats, ChatItemsList } from "..";
|
||||||
|
|
||||||
|
const MAX_HIDE_VISIBLE_COUNT = 3;
|
||||||
|
|
||||||
interface ChatCategoriesProps {
|
interface ChatCategoriesProps {
|
||||||
chatsPromise: Promise<ICategorizedChats>;
|
chatsPromise: Promise<IGetChatsListResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatCategories({ chatsPromise }: ChatCategoriesProps) {
|
export default function ChatCategories({ chatsPromise }: ChatCategoriesProps) {
|
||||||
const chats = use(chatsPromise);
|
const chats = use(chatsPromise);
|
||||||
|
const { categorizedChats } = useChatsSocket({ initialChats: chats });
|
||||||
|
|
||||||
const [activeChip, setActiveChip] = useState<string>("All");
|
const [activeChip, setActiveChip] = useState<string>("All");
|
||||||
const [maxVisibleChats, setMaxVisibleChats] = useState<
|
const [maxVisibleChats, setMaxVisibleChats] = useState<
|
||||||
Partial<Record<string, number | null>>
|
Partial<Record<string, number | null>>
|
||||||
>({});
|
>({});
|
||||||
|
|
||||||
const chips = Object.keys(chats).map(key => ({
|
const chips = Object.keys(categorizedChats).map(key => ({
|
||||||
text: key,
|
text: key,
|
||||||
}));
|
}));
|
||||||
chips.unshift({
|
chips.unshift({
|
||||||
text: "All",
|
text: "All",
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredChats = Object.keys(chats).filter(key => {
|
const filteredChats = Object.keys(categorizedChats).filter(key => {
|
||||||
if (activeChip === "All") return true;
|
if (activeChip === "All") return true;
|
||||||
return chats[key].some(chat => chat.category === activeChip);
|
return categorizedChats[key].some(chat => chat.category === activeChip);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -44,18 +49,22 @@ export default function ChatCategories({ chatsPromise }: ChatCategoriesProps) {
|
|||||||
title={key}
|
title={key}
|
||||||
key={key}
|
key={key}
|
||||||
viewAllProps={{
|
viewAllProps={{
|
||||||
count: chats[key].length,
|
count: categorizedChats[key].length,
|
||||||
|
isAll: !!maxVisibleChats[key],
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setMaxVisibleChats(prev => ({
|
setMaxVisibleChats(prev => ({
|
||||||
...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
|
<CategoryChats
|
||||||
chats={chats[key]}
|
chats={categorizedChats[key]}
|
||||||
maxVisibleChats={maxVisibleChats[key] ?? 3}
|
maxVisibleChats={maxVisibleChats[key] ?? MAX_HIDE_VISIBLE_COUNT}
|
||||||
/>
|
/>
|
||||||
</ChatItemsList>
|
</ChatItemsList>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,11 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { use, useEffect, useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
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 { useChat } from "@/providers/chat-provider";
|
||||||
import { useChatStore } from "@/providers/chat-store-provider";
|
import { useChatStore } from "@/providers/chat-store-provider";
|
||||||
import { formatSecondsToHHMMSS } from "@/shared/utils/date";
|
import { formatSecondsToHHMMSS } from "@/shared/utils/date";
|
||||||
@ -13,12 +22,17 @@ import { delay } from "@/shared/utils/delay";
|
|||||||
|
|
||||||
import styles from "./ChatHeader.module.scss";
|
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 t = useTranslations("Chat");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const currentChat = useChatStore(state => state.currentChat);
|
const currentChat = useChatStore(state => state.currentChat);
|
||||||
const { isLoadingAdvisorMessage, isAvailableChatting } = useChat();
|
const { isLoadingAdvisorMessage, isAvailableChatting } = useChat();
|
||||||
|
const chats = use(chatsPromise);
|
||||||
|
const { totalUnreadCount } = useChatsSocket({ initialChats: chats });
|
||||||
const [timer, setTimer] = useState(0);
|
const [timer, setTimer] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -30,19 +44,26 @@ export default function ChatHeader() {
|
|||||||
})();
|
})();
|
||||||
}, [isAvailableChatting, timer]);
|
}, [isAvailableChatting, timer]);
|
||||||
|
|
||||||
|
const handleBack = async () => {
|
||||||
|
await revalidateChatsPage();
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<div className={styles.back} onClick={() => router.back()}>
|
<div className={styles.back} onClick={handleBack}>
|
||||||
<Icon
|
<Icon
|
||||||
name={IconName.ChevronLeft}
|
name={IconName.ChevronLeft}
|
||||||
size={{ height: 22, width: 22 }}
|
size={{ height: 22, width: 22 }}
|
||||||
color="#374151"
|
color="#374151"
|
||||||
/>
|
/>
|
||||||
{/* <Badge className={styles.badge}>
|
{!!totalUnreadCount && (
|
||||||
<Typography weight="semiBold" size="xs" color="black">
|
<Badge className={styles.badge}>
|
||||||
2
|
<Typography weight="semiBold" size="xs" color="black">
|
||||||
</Typography>
|
{totalUnreadCount}
|
||||||
</Badge> */}
|
</Typography>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.chatInfo}>
|
<div className={styles.chatInfo}>
|
||||||
{!!currentChat?.assistantAvatar ? (
|
{!!currentChat?.assistantAvatar ? (
|
||||||
@ -60,7 +81,7 @@ export default function ChatHeader() {
|
|||||||
<Typography weight="semiBold" className={styles.name}>
|
<Typography weight="semiBold" className={styles.name}>
|
||||||
{currentChat?.assistantName}
|
{currentChat?.assistantName}
|
||||||
<OnlineIndicator
|
<OnlineIndicator
|
||||||
isOnline={currentChat?.status === "inactive"}
|
isOnline={currentChat?.status === "active"}
|
||||||
className={styles.onlineIndicator}
|
className={styles.onlineIndicator}
|
||||||
/>
|
/>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@ -11,6 +11,7 @@ interface ChatItemsListProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
viewAllProps: ViewAllProps;
|
viewAllProps: ViewAllProps;
|
||||||
|
isVisibleViewAll?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatItemsList({
|
export default function ChatItemsList({
|
||||||
@ -18,10 +19,15 @@ export default function ChatItemsList({
|
|||||||
children,
|
children,
|
||||||
title,
|
title,
|
||||||
viewAllProps,
|
viewAllProps,
|
||||||
|
isVisibleViewAll = true,
|
||||||
}: ChatItemsListProps) {
|
}: ChatItemsListProps) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.chatItemsList, className)}>
|
<div className={clsx(styles.chatItemsList, className)}>
|
||||||
<ChatItemsListHeader title={title} viewAllProps={viewAllProps} />
|
<ChatItemsListHeader
|
||||||
|
title={title}
|
||||||
|
viewAllProps={viewAllProps}
|
||||||
|
isVisibleViewAll={isVisibleViewAll}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,18 +7,20 @@ import styles from "./ChatItemsListHeader.module.scss";
|
|||||||
interface ChatItemsListHeaderProps {
|
interface ChatItemsListHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
viewAllProps: ViewAllProps;
|
viewAllProps: ViewAllProps;
|
||||||
|
isVisibleViewAll?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatItemsListHeader({
|
export default function ChatItemsListHeader({
|
||||||
title,
|
title,
|
||||||
viewAllProps,
|
viewAllProps,
|
||||||
|
isVisibleViewAll = true,
|
||||||
}: ChatItemsListHeaderProps) {
|
}: ChatItemsListHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.chatItemsListHeader}>
|
<div className={styles.chatItemsListHeader}>
|
||||||
<Typography className={styles.title} as="h3" size="lg" weight="bold">
|
<Typography className={styles.title} as="h3" size="lg" weight="bold">
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<ViewAll {...viewAllProps} />
|
{isVisibleViewAll && <ViewAll {...viewAllProps} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,12 @@ export default function ChatMessage({ message }: ChatMessageProps) {
|
|||||||
const { read } = useChat();
|
const { read } = useChat();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!!message.id && !message.isRead) {
|
if (
|
||||||
|
!!message.id &&
|
||||||
|
!message.isRead &&
|
||||||
|
message.id !== "typing" &&
|
||||||
|
!message.id.startsWith("sending-message")
|
||||||
|
) {
|
||||||
read([message.id]);
|
read([message.id]);
|
||||||
}
|
}
|
||||||
}, [message.id, message.isRead, read]);
|
}, [message.id, message.isRead, read]);
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
padding: 36px 16px;
|
padding: 36px 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-end;
|
justify-content: flex-start;
|
||||||
|
flex-direction: column-reverse;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,9 +13,6 @@ export default function ChatMessages({
|
|||||||
}: ChatMessagesProps) {
|
}: ChatMessagesProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
{messages.map(message => (
|
|
||||||
<ChatMessage key={message.id} message={message} />
|
|
||||||
))}
|
|
||||||
{isLoadingAdvisorMessage && (
|
{isLoadingAdvisorMessage && (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
message={{
|
message={{
|
||||||
@ -28,6 +25,9 @@ export default function ChatMessages({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{messages.map(message => (
|
||||||
|
<ChatMessage key={message.id} message={message} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,5 +7,5 @@
|
|||||||
.loaderTop {
|
.loaderTop {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 8px 0;
|
padding-top: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo } from "react";
|
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
|
|
||||||
import { Spinner } from "@/components/ui";
|
import { Spinner } from "@/components/ui";
|
||||||
import { useChat } from "@/providers/chat-provider";
|
import { useChat } from "@/providers/chat-provider";
|
||||||
@ -17,15 +17,20 @@ export default function ChatMessagesWrapper() {
|
|||||||
hasMoreOlderMessages,
|
hasMoreOlderMessages,
|
||||||
isLoadingOlder,
|
isLoadingOlder,
|
||||||
messagesWrapperRef,
|
messagesWrapperRef,
|
||||||
|
loadOlder,
|
||||||
scrollToBottom,
|
scrollToBottom,
|
||||||
} = useChat();
|
} = useChat();
|
||||||
|
|
||||||
// const handleScroll = useCallback(() => {
|
const isInitialScrollDone = useRef(false);
|
||||||
// const el = messagesWrapperRef.current;
|
|
||||||
// if (!el) return;
|
|
||||||
|
|
||||||
// if (el.scrollTop < 120) loadOlder();
|
const handleScroll = useCallback(() => {
|
||||||
// }, [loadOlder]);
|
const el = messagesWrapperRef.current;
|
||||||
|
if (!el || !isInitialScrollDone.current) return;
|
||||||
|
|
||||||
|
if (el.scrollTop < 100) {
|
||||||
|
loadOlder();
|
||||||
|
}
|
||||||
|
}, [loadOlder, messagesWrapperRef]);
|
||||||
|
|
||||||
const mappedMessages = useMemo(() => {
|
const mappedMessages = useMemo(() => {
|
||||||
const msgs = socketMessages.map(m => ({
|
const msgs = socketMessages.map(m => ({
|
||||||
@ -40,14 +45,20 @@ export default function ChatMessagesWrapper() {
|
|||||||
}, [socketMessages]);
|
}, [socketMessages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
if (socketMessages.length > 0 && !isInitialScrollDone.current) {
|
||||||
}, [scrollToBottom]);
|
scrollToBottom();
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
isInitialScrollDone.current = true;
|
||||||
|
}, 1000);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}, [socketMessages.length, scrollToBottom]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.messagesWrapper}
|
className={styles.messagesWrapper}
|
||||||
ref={messagesWrapperRef}
|
ref={messagesWrapperRef}
|
||||||
// onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
>
|
>
|
||||||
{isLoadingOlder && hasMoreOlderMessages && (
|
{isLoadingOlder && hasMoreOlderMessages && (
|
||||||
<div className={styles.loaderTop}>
|
<div className={styles.loaderTop}>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
.container.container {
|
.card.card {
|
||||||
padding: 13px 0;
|
padding: 13px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,3 +21,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container.container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: fit-content;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|||||||
@ -12,36 +12,47 @@ import { formatTime } from "@/shared/utils/date";
|
|||||||
|
|
||||||
import styles from "./CorrespondenceStarted.module.scss";
|
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 {
|
interface CorrespondenceStartedProps {
|
||||||
chats: IChat[];
|
chats: IChat[];
|
||||||
maxVisibleChats?: number;
|
isVisibleAll?: boolean;
|
||||||
|
maxHideVisibleCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CorrespondenceStarted({
|
export default function CorrespondenceStarted({
|
||||||
chats,
|
chats,
|
||||||
maxVisibleChats = 3,
|
isVisibleAll = false,
|
||||||
|
maxHideVisibleCount = 3,
|
||||||
}: CorrespondenceStartedProps) {
|
}: CorrespondenceStartedProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations("Chat");
|
|
||||||
const setCurrentChat = useChatStore(state => state.setCurrentChat);
|
const setCurrentChat = useChatStore(state => state.setCurrentChat);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={styles.container}>
|
<Container isVisibleAll={isVisibleAll} chats={chats}>
|
||||||
<div className={styles.header}>
|
{chats
|
||||||
<Icon name={IconName.Pin} size={{ height: 17, width: 13.5 }} />
|
.slice(0, isVisibleAll ? chats.length : maxHideVisibleCount)
|
||||||
<Typography size="sm" color="muted">
|
.map((chat, index) => (
|
||||||
{t("correspondence_started.pinned_chats")}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<div className={styles.chats}>
|
|
||||||
{chats.slice(0, maxVisibleChats).map(chat => (
|
|
||||||
<ChatItem
|
<ChatItem
|
||||||
key={chat.id}
|
key={chat.id}
|
||||||
className={styles.chat}
|
className={styles.chat}
|
||||||
|
style={
|
||||||
|
!isVisibleAll
|
||||||
|
? {
|
||||||
|
top: `${getTopPositionItem(index)}px`,
|
||||||
|
zIndex: 1111 - index,
|
||||||
|
position: !!index ? "absolute" : "relative",
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
userAvatar={{
|
userAvatar={{
|
||||||
src: chat.assistantAvatar,
|
src: chat.assistantAvatar,
|
||||||
alt: chat.assistantName,
|
alt: chat.assistantName,
|
||||||
isOnline: true,
|
isOnline: chat.status === "active",
|
||||||
}}
|
}}
|
||||||
name={chat.assistantName}
|
name={chat.assistantName}
|
||||||
messagePreiew={
|
messagePreiew={
|
||||||
@ -62,7 +73,43 @@ export default function CorrespondenceStarted({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Container>
|
||||||
</Card>
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { use, useState } from "react";
|
import { use } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { Skeleton } from "@/components/ui";
|
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 "..";
|
import { ChatItemsList, CorrespondenceStarted } from "..";
|
||||||
|
|
||||||
interface CorrespondenceStartedWrapperProps {
|
interface CorrespondenceStartedWrapperProps {
|
||||||
chatsPromise: Promise<IChat[]>;
|
chatsPromise: Promise<IGetChatsListResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CorrespondenceStartedWrapper({
|
export default function CorrespondenceStartedWrapper({
|
||||||
@ -17,24 +19,37 @@ export default function CorrespondenceStartedWrapper({
|
|||||||
}: CorrespondenceStartedWrapperProps) {
|
}: CorrespondenceStartedWrapperProps) {
|
||||||
const t = useTranslations("Chat");
|
const t = useTranslations("Chat");
|
||||||
const chats = use(chatsPromise);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{!!chats.length && (
|
{!!startedChats.length && (
|
||||||
<ChatItemsList
|
<ChatItemsList
|
||||||
title={t("correspondence_started.title")}
|
title={t("correspondence_started.title")}
|
||||||
viewAllProps={{
|
viewAllProps={{
|
||||||
count: chats.length,
|
count: startedChats.length,
|
||||||
|
isAll: isVisibleAll,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setMaxVisibleChats(prev => (prev ? null : chats.length));
|
setChatsCorrespondenceStarted({
|
||||||
|
isVisibleAll: !isVisibleAll,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
isVisibleViewAll={startedChats.length > 1}
|
||||||
>
|
>
|
||||||
<CorrespondenceStarted
|
<CorrespondenceStarted
|
||||||
chats={chats}
|
chats={startedChats}
|
||||||
maxVisibleChats={maxVisibleChats ?? 3}
|
isVisibleAll={isVisibleAll}
|
||||||
/>
|
/>
|
||||||
</ChatItemsList>
|
</ChatItemsList>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
.container {
|
.container.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
@ -11,3 +11,18 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
import { Card } from "@/components/ui";
|
||||||
import { ChatItem } from "@/components/widgets";
|
import { ChatItem } from "@/components/widgets";
|
||||||
import { IChat } from "@/entities/chats/types";
|
import { IChat } from "@/entities/chats/types";
|
||||||
import { useChatStore } from "@/providers/chat-store-provider";
|
import { useChatStore } from "@/providers/chat-store-provider";
|
||||||
@ -19,14 +20,74 @@ const getTopPositionItem = (index: number) => {
|
|||||||
interface NewMessagesProps {
|
interface NewMessagesProps {
|
||||||
chats: IChat[];
|
chats: IChat[];
|
||||||
isVisibleAll: boolean;
|
isVisibleAll: boolean;
|
||||||
|
maxHideVisibleCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NewMessages({
|
export default function NewMessages({
|
||||||
chats,
|
chats,
|
||||||
isVisibleAll = false,
|
isVisibleAll = false,
|
||||||
|
maxHideVisibleCount = 3,
|
||||||
}: NewMessagesProps) {
|
}: NewMessagesProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const setCurrentChat = useChatStore(state => state.setCurrentChat);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.container}
|
className={styles.container}
|
||||||
@ -34,43 +95,7 @@ export default function NewMessages({
|
|||||||
paddingBottom: getTopPositionItem(chats.length - 1),
|
paddingBottom: getTopPositionItem(chats.length - 1),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{chats.map((chat, index) => (
|
{children}
|
||||||
<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));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { use, useState } from "react";
|
import { use } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { Skeleton } from "@/components/ui";
|
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 "..";
|
import { ChatItemsList, NewMessages } from "..";
|
||||||
|
|
||||||
interface NewMessagesWrapperProps {
|
interface NewMessagesWrapperProps {
|
||||||
chatsPromise: Promise<IChat[]>;
|
chatsPromise: Promise<IGetChatsListResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NewMessagesWrapper({
|
export default function NewMessagesWrapper({
|
||||||
@ -17,22 +19,29 @@ export default function NewMessagesWrapper({
|
|||||||
}: NewMessagesWrapperProps) {
|
}: NewMessagesWrapperProps) {
|
||||||
const t = useTranslations("Chat");
|
const t = useTranslations("Chat");
|
||||||
const chats = use(chatsPromise);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{!!chats.length && (
|
{!!unreadChats.length && (
|
||||||
<ChatItemsList
|
<ChatItemsList
|
||||||
title={t("new_messages")}
|
title={t("new_messages")}
|
||||||
viewAllProps={{
|
viewAllProps={{
|
||||||
count: chats.length,
|
count: unreadChats.length,
|
||||||
|
isAll: isVisibleAll,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setIsVisibleAll(prev => !prev);
|
setChatsNewMessages({ isVisibleAll: !isVisibleAll });
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
isVisibleViewAll={unreadChats.length > 1}
|
||||||
>
|
>
|
||||||
<NewMessages chats={chats} isVisibleAll={isVisibleAll} />
|
<NewMessages chats={unreadChats} isVisibleAll={isVisibleAll} />
|
||||||
</ChatItemsList>
|
</ChatItemsList>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -7,19 +7,18 @@ import { Button, Typography } from "@/components/ui";
|
|||||||
import styles from "./ViewAll.module.scss";
|
import styles from "./ViewAll.module.scss";
|
||||||
|
|
||||||
export interface ViewAllProps {
|
export interface ViewAllProps {
|
||||||
|
isAll: boolean;
|
||||||
count: number;
|
count: number;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ViewAll({ count, onClick }: ViewAllProps) {
|
export default function ViewAll({ count, isAll, onClick }: ViewAllProps) {
|
||||||
const t = useTranslations("Chat");
|
const t = useTranslations("Chat");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button className={styles.viewAllButton} onClick={onClick}>
|
<Button className={styles.viewAllButton} onClick={onClick}>
|
||||||
<Typography size="sm" weight="medium" color="muted">
|
<Typography size="sm" weight="medium" color="muted">
|
||||||
{t("view_all", {
|
{isAll ? t("hide_all") : t("view_all", { count })}
|
||||||
count,
|
|
||||||
})}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,21 +1,35 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
import { Button, Card, Stars, Typography } from "@/components/ui";
|
import { Button, Card, Stars, Typography } from "@/components/ui";
|
||||||
|
import { IChat } from "@/entities/chats/types";
|
||||||
import { Assistant } from "@/entities/dashboard/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";
|
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 (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={styles.card}
|
className={styles.card}
|
||||||
style={{ backgroundImage: `url(${photoUrl})` }}
|
style={{ backgroundImage: `url(${photoUrl})` }}
|
||||||
|
onClick={() => {
|
||||||
|
if (chat) {
|
||||||
|
setCurrentChat(chat);
|
||||||
|
}
|
||||||
|
router.push(ROUTES.chat(_id));
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { use } from "react";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import { Grid, Section, Skeleton } from "@/components/ui";
|
import { Grid, Section, Skeleton } from "@/components/ui";
|
||||||
|
import { IChat, IGetChatsListResponse } from "@/entities/chats/types";
|
||||||
import { Assistant } from "@/entities/dashboard/types";
|
import { Assistant } from "@/entities/dashboard/types";
|
||||||
|
|
||||||
import { AdviserCard } from "../../cards";
|
import { AdviserCard } from "../../cards";
|
||||||
@ -9,15 +10,22 @@ import { AdviserCard } from "../../cards";
|
|||||||
import styles from "./AdvisersSection.module.scss";
|
import styles from "./AdvisersSection.module.scss";
|
||||||
|
|
||||||
interface AdvisersSectionProps {
|
interface AdvisersSectionProps {
|
||||||
promise: Promise<Assistant[]>;
|
promiseAssistants: Promise<Assistant[]>;
|
||||||
|
promiseChats: Promise<IGetChatsListResponse>;
|
||||||
gridDisplayMode?: "vertical" | "horizontal";
|
gridDisplayMode?: "vertical" | "horizontal";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getChatByAssistantId = (assistantId: string, chats: IChat[]) => {
|
||||||
|
return chats.find(chat => chat.assistantId === assistantId) || null;
|
||||||
|
};
|
||||||
|
|
||||||
export default function AdvisersSection({
|
export default function AdvisersSection({
|
||||||
promise,
|
promiseAssistants,
|
||||||
|
promiseChats,
|
||||||
gridDisplayMode = "horizontal",
|
gridDisplayMode = "horizontal",
|
||||||
}: AdvisersSectionProps) {
|
}: AdvisersSectionProps) {
|
||||||
const assistants = use(promise);
|
const assistants = use(promiseAssistants);
|
||||||
|
const chats = use(promiseChats);
|
||||||
const columns = Math.ceil(assistants?.length / 2);
|
const columns = Math.ceil(assistants?.length / 2);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -27,7 +35,14 @@ export default function AdvisersSection({
|
|||||||
className={clsx(styles.grid, styles[gridDisplayMode])}
|
className={clsx(styles.grid, styles[gridDisplayMode])}
|
||||||
>
|
>
|
||||||
{assistants.map(adviser => (
|
{assistants.map(adviser => (
|
||||||
<AdviserCard key={adviser._id} {...adviser} />
|
<AdviserCard
|
||||||
|
key={adviser._id}
|
||||||
|
assistant={adviser}
|
||||||
|
chat={getChatByAssistantId(
|
||||||
|
adviser._id,
|
||||||
|
chats.categorizedChats[adviser.category]
|
||||||
|
)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
@ -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%" }} />;
|
||||||
|
};
|
||||||
@ -10,6 +10,10 @@ export {
|
|||||||
default as MeditationSection,
|
default as MeditationSection,
|
||||||
MeditationSectionSkeleton,
|
MeditationSectionSkeleton,
|
||||||
} from "./MeditationSection/MeditationSection";
|
} from "./MeditationSection/MeditationSection";
|
||||||
|
export {
|
||||||
|
default as NewMessagesSection,
|
||||||
|
NewMessagesSectionSkeleton,
|
||||||
|
} from "./NewMessagesSection/NewMessagesSection";
|
||||||
export {
|
export {
|
||||||
default as PalmSection,
|
default as PalmSection,
|
||||||
PalmSectionSkeleton,
|
PalmSectionSkeleton,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: start;
|
align-items: flex-start;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { use } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import { Button, Icon, IconName } from "@/components/ui";
|
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 { ROUTES } from "@/shared/constants/client-routes";
|
||||||
|
|
||||||
import { useDrawer } from "..";
|
import { useDrawer } from "..";
|
||||||
@ -13,10 +16,13 @@ import styles from "./Header.module.scss";
|
|||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
chatsPromise: Promise<IGetChatsListResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Header({ className }: HeaderProps) {
|
export default function Header({ className, chatsPromise }: HeaderProps) {
|
||||||
const { open } = useDrawer();
|
const { open } = useDrawer();
|
||||||
|
const chats = use(chatsPromise);
|
||||||
|
const { totalUnreadCount } = useChatsSocket({ initialChats: chats });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={clsx(styles.header, className)}>
|
<header className={clsx(styles.header, className)}>
|
||||||
@ -27,7 +33,9 @@ export default function Header({ className }: HeaderProps) {
|
|||||||
<Logo />
|
<Logo />
|
||||||
</Link>
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<Icon name={IconName.Notification} />
|
<Link href={ROUTES.chat()}>
|
||||||
|
<Icon name={IconName.Notification} iconChildren={totalUnreadCount} />
|
||||||
|
</Link>
|
||||||
<Icon name={IconName.Search} />
|
<Icon name={IconName.Search} />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@ -1,21 +1,35 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { use } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useLocale } from "next-intl";
|
import { useLocale } from "next-intl";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import { Badge, Icon, Typography } from "@/components/ui";
|
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 { 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 { stripLocale } from "@/shared/utils/path";
|
||||||
|
|
||||||
import styles from "./NavigationBar.module.scss";
|
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 pathname = usePathname();
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
const pathnameWithoutLocale = stripLocale(pathname, locale);
|
const pathnameWithoutLocale = stripLocale(pathname, locale);
|
||||||
|
const chats = use(chatsPromise);
|
||||||
|
const { totalUnreadCount } = useChatsSocket({ initialChats: chats });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={styles.container}>
|
<nav className={styles.container}>
|
||||||
@ -25,6 +39,8 @@ export default function NavigationBar() {
|
|||||||
? pathnameWithoutLocale === item.href
|
? pathnameWithoutLocale === item.href
|
||||||
: pathnameWithoutLocale.startsWith(item.href);
|
: pathnameWithoutLocale.startsWith(item.href);
|
||||||
|
|
||||||
|
const badge = getBadge(item, totalUnreadCount);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={item.key}
|
key={item.key}
|
||||||
@ -32,10 +48,10 @@ export default function NavigationBar() {
|
|||||||
className={clsx(styles.item, { [styles.active]: isActive })}
|
className={clsx(styles.item, { [styles.active]: isActive })}
|
||||||
>
|
>
|
||||||
<Icon name={item.icon} color={isActive ? "#007AFF" : "#8A8D93"}>
|
<Icon name={item.icon} color={isActive ? "#007AFF" : "#8A8D93"}>
|
||||||
{item.badge && (
|
{!!badge && (
|
||||||
<Badge className={styles.badge}>
|
<Badge className={styles.badge}>
|
||||||
<Typography weight="medium" size="xs" color="white">
|
<Typography weight="medium" size="xs" color="white">
|
||||||
{item.badge}
|
{badge}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -94,6 +94,7 @@ export type IconProps = {
|
|||||||
cursor?: "pointer" | "auto";
|
cursor?: "pointer" | "auto";
|
||||||
iconStyle?: CSSProperties;
|
iconStyle?: CSSProperties;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
|
iconChildren?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Icon({
|
export default function Icon({
|
||||||
@ -107,6 +108,7 @@ export default function Icon({
|
|||||||
children,
|
children,
|
||||||
cursor = "pointer",
|
cursor = "pointer",
|
||||||
style,
|
style,
|
||||||
|
iconChildren,
|
||||||
...rest
|
...rest
|
||||||
}: IconProps) {
|
}: IconProps) {
|
||||||
const Component = icons[name];
|
const Component = icons[name];
|
||||||
@ -132,7 +134,9 @@ export default function Icon({
|
|||||||
display: "block",
|
display: "block",
|
||||||
...rest.iconStyle,
|
...rest.iconStyle,
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
{iconChildren}
|
||||||
|
</Component>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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"
|
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"
|
stroke="#E5E7EB"
|
||||||
/>
|
/>
|
||||||
<path
|
<text x="12" y="12" fill="white" fontSize="10">
|
||||||
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"
|
{props.children}
|
||||||
fill="white"
|
</text>
|
||||||
/>
|
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="clip0_20_1490">
|
<clipPath id="clip0_20_1490">
|
||||||
<rect
|
<rect
|
||||||
|
|||||||
@ -1,9 +1,16 @@
|
|||||||
.onlineIndicator {
|
.onlineIndicator {
|
||||||
aspect-ratio: 1/1;
|
aspect-ratio: 1/1;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: #10b981;
|
|
||||||
border: 2px solid #fff;
|
border: 2px solid #fff;
|
||||||
|
|
||||||
|
&.online {
|
||||||
|
background-color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.offline {
|
||||||
|
background-color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
&.sm {
|
&.sm {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,10 +15,12 @@ export default function OnlineIndicator({
|
|||||||
}: OnlineIndicatorProps) {
|
}: OnlineIndicatorProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.onlineIndicator, styles[size], className)}
|
className={clsx(
|
||||||
style={{
|
styles.onlineIndicator,
|
||||||
backgroundColor: isOnline ? "#10b981" : "#9CA3AF",
|
styles[size],
|
||||||
}}
|
isOnline ? styles.online : styles.offline,
|
||||||
|
className
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,11 +8,12 @@
|
|||||||
background: #f3f4f6;
|
background: #f3f4f6;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: 1px solid #191f29;
|
border: 1px solid #191f29;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
min-height: 94px;
|
min-height: 94px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { revalidateTag } from "next/cache";
|
||||||
|
|
||||||
import { http } from "@/shared/api/httpClient";
|
import { http } from "@/shared/api/httpClient";
|
||||||
import { API_ROUTES } from "@/shared/constants/api-routes";
|
import { API_ROUTES } from "@/shared/constants/api-routes";
|
||||||
import { ActionResponse } from "@/types";
|
import { ActionResponse } from "@/types";
|
||||||
@ -57,3 +59,7 @@ export async function fetchChatMessages(
|
|||||||
return { data: null, error: errorMessage };
|
return { data: null, error: errorMessage };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function revalidateChatsPage() {
|
||||||
|
revalidateTag("chats-list");
|
||||||
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export const createChat = async (
|
|||||||
|
|
||||||
export const getChatsList = async (): Promise<IGetChatsListResponse> => {
|
export const getChatsList = async (): Promise<IGetChatsListResponse> => {
|
||||||
return http.get<IGetChatsListResponse>(API_ROUTES.getChatsList(), {
|
return http.get<IGetChatsListResponse>(API_ROUTES.getChatsList(), {
|
||||||
tags: ["chats", "list"],
|
tags: ["chats-list"],
|
||||||
schema: GetChatsListResponseSchema,
|
schema: GetChatsListResponseSchema,
|
||||||
revalidate: 0,
|
revalidate: 0,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,18 +1,12 @@
|
|||||||
import { cache } from "react";
|
|
||||||
|
|
||||||
import { createAllChats, getChatMessages, getChatsList } from "./api";
|
import { createAllChats, getChatMessages, getChatsList } from "./api";
|
||||||
|
|
||||||
export const loadCreateAllChats = cache(createAllChats);
|
export const loadCreateAllChats = createAllChats;
|
||||||
|
|
||||||
export const loadChatsList = cache(getChatsList);
|
export const loadChatsList = getChatsList;
|
||||||
export const loadCategorizedChats = cache(() =>
|
export const loadCategorizedChats = () =>
|
||||||
loadChatsList().then(d => d.categorizedChats)
|
loadChatsList().then(d => d.categorizedChats);
|
||||||
);
|
export const loadUnreadChats = () => loadChatsList().then(d => d.unreadChats);
|
||||||
export const loadUnreadChats = cache(() =>
|
export const loadCorrespondenceStarted = () =>
|
||||||
loadChatsList().then(d => d.unreadChats)
|
loadChatsList().then(d => d.startedChats);
|
||||||
);
|
|
||||||
export const loadCorrespondenceStarted = cache(() =>
|
|
||||||
loadChatsList().then(d => d.startedChats)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const loadChatMessages = cache(getChatMessages);
|
export const loadChatMessages = getChatMessages;
|
||||||
|
|||||||
@ -63,9 +63,7 @@ export const useChatSocket = (
|
|||||||
const [refillModals, setRefillModals] = useState<IRefillModals | null>(null);
|
const [refillModals, setRefillModals] = useState<IRefillModals | null>(null);
|
||||||
|
|
||||||
const isLoadingAdvisorMessage = useMemo(() => {
|
const isLoadingAdvisorMessage = useMemo(() => {
|
||||||
return (
|
return messages.length > 0 && messages[0].role !== "assistant";
|
||||||
messages.length > 0 && messages[messages.length - 1].role !== "assistant"
|
|
||||||
);
|
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
const joinChat = useCallback(
|
const joinChat = useCallback(
|
||||||
@ -86,7 +84,7 @@ export const useChatSocket = (
|
|||||||
createdDate: new Date().toISOString(),
|
createdDate: new Date().toISOString(),
|
||||||
isRead: false,
|
isRead: false,
|
||||||
};
|
};
|
||||||
setMessages(prev => [...prev, sendingMessage]);
|
setMessages(prev => [sendingMessage, ...prev]);
|
||||||
if (options.onNewMessage) {
|
if (options.onNewMessage) {
|
||||||
options.onNewMessage(sendingMessage);
|
options.onNewMessage(sendingMessage);
|
||||||
}
|
}
|
||||||
@ -145,8 +143,8 @@ export const useChatSocket = (
|
|||||||
setMessages(prev => {
|
setMessages(prev => {
|
||||||
const ids = new Set(prev.map(m => m.id));
|
const ids = new Set(prev.map(m => m.id));
|
||||||
return [
|
return [
|
||||||
...msgs.map(mapApiMessage).filter(m => !ids.has(m.id)),
|
|
||||||
...prev,
|
...prev,
|
||||||
|
...msgs.map(mapApiMessage).filter(m => !ids.has(m.id)),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
setPage(nextPage);
|
setPage(nextPage);
|
||||||
@ -182,7 +180,7 @@ export const useChatSocket = (
|
|||||||
);
|
);
|
||||||
return Array.from(map.values()).sort(
|
return Array.from(map.values()).sort(
|
||||||
(a, b) =>
|
(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_started", s => setSession(s.data));
|
||||||
useSocketEvent("session_ended", () => setSession(null));
|
useSocketEvent("session_ended", () => setSession(null));
|
||||||
useSocketEvent("show_refill_modals", r => setRefillModals(r));
|
useSocketEvent("show_refill_modals", r => setRefillModals(r.data));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!session?.maxFinishedAt) return;
|
if (!session?.maxFinishedAt) return;
|
||||||
|
|||||||
41
src/hooks/chats/useChatsSocket.ts
Normal file
41
src/hooks/chats/useChatsSocket.ts
Normal 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]
|
||||||
|
);
|
||||||
|
};
|
||||||
39
src/providers/app-ui-store-provider.tsx
Normal file
39
src/providers/app-ui-store-provider.tsx
Normal 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);
|
||||||
|
};
|
||||||
@ -13,7 +13,7 @@ import { useChatSocket } from "@/hooks/chats/useChatSocket";
|
|||||||
|
|
||||||
interface ChatContextValue extends ReturnType<typeof useChatSocket> {
|
interface ChatContextValue extends ReturnType<typeof useChatSocket> {
|
||||||
messagesWrapperRef: React.RefObject<HTMLDivElement | null>;
|
messagesWrapperRef: React.RefObject<HTMLDivElement | null>;
|
||||||
scrollToBottom: () => void;
|
scrollToBottom: (behavior?: ScrollBehavior) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatContext = createContext<ChatContextValue | null>(null);
|
const ChatContext = createContext<ChatContextValue | null>(null);
|
||||||
@ -47,11 +47,11 @@ export function ChatProvider({
|
|||||||
|
|
||||||
const messagesWrapperRef = useRef<HTMLDivElement>(null);
|
const messagesWrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const scrollToBottom = useCallback(() => {
|
const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
|
||||||
if (messagesWrapperRef.current) {
|
if (messagesWrapperRef.current) {
|
||||||
messagesWrapperRef.current.scrollTo({
|
messagesWrapperRef.current.scrollTo({
|
||||||
top: messagesWrapperRef.current.scrollHeight,
|
top: messagesWrapperRef.current.scrollHeight,
|
||||||
behavior: "smooth",
|
behavior,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||||
import { Currency } from "@/types";
|
import { Currency } from "@/types";
|
||||||
|
|
||||||
export interface IMessage {
|
export interface IMessage {
|
||||||
@ -55,6 +56,10 @@ export interface IRefillModals {
|
|||||||
products?: IRefillModalsProduct[];
|
products?: IRefillModalsProduct[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IUnreadMessagesCount {
|
||||||
|
unreadCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ClientToServerEvents {
|
export interface ClientToServerEvents {
|
||||||
join_chat: (data: { chatId: string }) => void;
|
join_chat: (data: { chatId: string }) => void;
|
||||||
leave_chat: (data: { chatId: string }) => void;
|
leave_chat: (data: { chatId: string }) => void;
|
||||||
@ -85,5 +90,9 @@ export interface ServerToClientEvents {
|
|||||||
data: ServerToClientEventsBaseData<ISessionStarted>
|
data: ServerToClientEventsBaseData<ISessionStarted>
|
||||||
) => void;
|
) => void;
|
||||||
session_ended: (data: ServerToClientEventsBaseData<boolean>) => 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,12 +2,12 @@ import { IconName } from "@/components/ui";
|
|||||||
|
|
||||||
import { ROUTES } from "./client-routes";
|
import { ROUTES } from "./client-routes";
|
||||||
|
|
||||||
interface NavItem {
|
export interface NavItem {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon: IconName;
|
icon: IconName;
|
||||||
href: string;
|
href: string;
|
||||||
badge?: number;
|
badgeId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const navItems: NavItem[] = [
|
export const navItems: NavItem[] = [
|
||||||
@ -17,13 +17,13 @@ export const navItems: NavItem[] = [
|
|||||||
icon: IconName.Home,
|
icon: IconName.Home,
|
||||||
href: ROUTES.home(),
|
href: ROUTES.home(),
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// key: "chat",
|
key: "chat",
|
||||||
// label: "Chat",
|
label: "Chat",
|
||||||
// icon: IconName.Chat,
|
icon: IconName.Chat,
|
||||||
// href: ROUTES.chat(),
|
href: ROUTES.chat(),
|
||||||
// badge: 12,
|
badgeId: "unreadCount",
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
key: "advisers",
|
key: "advisers",
|
||||||
label: "Advi...",
|
label: "Advi...",
|
||||||
|
|||||||
102
src/stores/app-ui-store.ts
Normal file
102
src/stores/app-ui-store.ts
Normal 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user