- Product-page chat buttons pass the product id when starting a thread, so it's shared into the conversation as a card the seller can see (and tap through). - MessageBubble renders the product card (image, title, price → product page); card-only messages no longer render an empty bubble. - Inbox preview shows 🛍 <product> instead of "بدون پیام" for product-card threads. ChatProductCard type + product on message/last-message types. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
172 lines
5.7 KiB
TypeScript
172 lines
5.7 KiB
TypeScript
import { Link } from "@remix-run/react";
|
|
import AppInput from "../AppInput";
|
|
import { useState } from "react";
|
|
import {
|
|
useGetUserThreads,
|
|
useGetStoreThreads,
|
|
} from "../../requestHandler/use-chat-hooks";
|
|
import {
|
|
ThreadList,
|
|
ThreadParticipant,
|
|
UserProfile,
|
|
} from "../../../src/api/types";
|
|
import { useServiceGetProfile } from "../../requestHandler/use-profile-hooks";
|
|
import UiProvider from "../UiProvider";
|
|
import SellerLogo from "../SellerLogo";
|
|
import { Search, User } from "lucide-react";
|
|
import ProfilePagesHeader from "../ProfilePagesHeader";
|
|
|
|
interface ThreadsPageProps {
|
|
type: "user" | "store";
|
|
storeId?: string;
|
|
}
|
|
|
|
export default function ThreadsPage({ type, storeId }: ThreadsPageProps) {
|
|
const [searchValue, setSearchValue] = useState("");
|
|
// Seller dashboard shows the STORE's inbox; the profile page shows the user's
|
|
// personal (buyer) chats. Two different endpoints so the two never mix.
|
|
const userThreads = useGetUserThreads();
|
|
const storeThreads = useGetStoreThreads(type === "store" ? storeId : undefined);
|
|
const {
|
|
data: threads,
|
|
isLoading,
|
|
isError,
|
|
refetch,
|
|
} = type === "store" ? storeThreads : userThreads;
|
|
const sortedThreads = threads?.sort((a, b) => {
|
|
const dateA = new Date(a.lastMessageTime || "2025-01-18T00:00:00.000Z");
|
|
const dateB = new Date(b.lastMessageTime || "2025-01-18T00:00:00.000Z");
|
|
return dateB.getTime() - dateA.getTime();
|
|
});
|
|
const { data: dataProfile, isFetching: isFetchingProfile } =
|
|
useServiceGetProfile();
|
|
|
|
return (
|
|
<div
|
|
className={`flex flex-col w-full ${isLoading || isFetchingProfile ? "" : "pb-20"}`}
|
|
>
|
|
<ProfilePagesHeader title="پیامها" />
|
|
<div className="w-full px-4 py-2">
|
|
<div className="w-full h-[40px] flex items-center gap-2 bg-WHITE2 rounded-[10px]">
|
|
<AppInput
|
|
inputIcon={Search}
|
|
inputDir="rtl"
|
|
inputTitle={""}
|
|
className="bg-WHITE2"
|
|
inputValue={searchValue}
|
|
inputPlaceholder={"جستجو..."}
|
|
inputOnChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const value = e.target.value;
|
|
setSearchValue(value);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<UiProvider
|
|
isLoading={isLoading || isFetchingProfile}
|
|
isError={isError}
|
|
isEmpty={!!(threads && threads.length === 0)}
|
|
onRetry={refetch}
|
|
type="threads"
|
|
>
|
|
<ThreadsList
|
|
searchValue={searchValue}
|
|
threads={sortedThreads}
|
|
dataProfile={dataProfile}
|
|
type={type}
|
|
storeId={storeId}
|
|
/>
|
|
</UiProvider>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const ThreadsList = ({
|
|
searchValue,
|
|
threads,
|
|
dataProfile,
|
|
type,
|
|
storeId,
|
|
}: {
|
|
searchValue: string;
|
|
threads: ThreadList[] | undefined;
|
|
dataProfile: UserProfile | undefined;
|
|
type: "user" | "store";
|
|
storeId?: string;
|
|
}) => {
|
|
const filteredThreads = threads?.filter((thread) => {
|
|
return thread.participants?.some((participant: ThreadParticipant) =>
|
|
participant.username?.toLowerCase().includes(searchValue.toLowerCase())
|
|
);
|
|
});
|
|
|
|
return (
|
|
<div className="flex flex-col w-full">
|
|
{filteredThreads?.map((thread) => {
|
|
const otherParticipant = thread.participants?.find(
|
|
(participant: ThreadParticipant) =>
|
|
participant.username !== dataProfile?.username
|
|
);
|
|
const hasUnread = Number(thread.unreadCount || "0") > 0;
|
|
|
|
// Generate link based on type
|
|
const linkTo =
|
|
type === "user"
|
|
? `/profile/threads/${thread.id}`
|
|
: `/store/${storeId}/threads/${thread.id}`;
|
|
|
|
return (
|
|
<Link to={linkTo} key={thread.id} className="w-full">
|
|
<div className={`w-full h-[80px] flex items-center px-4 gap-4`}>
|
|
{thread.isCreator ? (
|
|
<SellerLogo
|
|
src={otherParticipant?.storeLogo}
|
|
alt="Participant Avatar"
|
|
/>
|
|
) : (
|
|
<User size={48} className="text-GRAY" />
|
|
)}
|
|
<div className="flex flex-col w-full">
|
|
<div className="w-full flex justify-between items-center">
|
|
<p className={`text-R14 ${hasUnread ? "font-bold" : ""}`}>
|
|
{thread.isCreator
|
|
? otherParticipant?.storeName ||
|
|
otherParticipant?.username
|
|
: otherParticipant?.name}
|
|
</p>
|
|
<div className="flex flex-col items-center justify-center">
|
|
{thread.lastMessageTime && (
|
|
<p className="text-R10 text-GRAY">
|
|
{new Date(thread.lastMessageTime).toLocaleTimeString(
|
|
"fa-IR",
|
|
{
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
}
|
|
)}
|
|
</p>
|
|
)}
|
|
{hasUnread && (
|
|
<div className="w-5 h-5 flex items-center justify-center bg-blue-500 text-WHITE text-R10 rounded-full">
|
|
{thread.unreadCount}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<p
|
|
className={`text-R14 ${hasUnread ? "text-BLACK font-bold" : "text-GRAY"}`}
|
|
>
|
|
{thread.lastMessage?.content ||
|
|
(thread.lastMessage?.product
|
|
? `🛍 ${thread.lastMessage.product.title || "محصول"}`
|
|
: "بدون پیام")}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
};
|