feat(chat): share the product into the chat + fix empty-thread preview
- 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>
This commit is contained in:
parent
d694040bf7
commit
7867db425e
@ -19,11 +19,14 @@ export const ContactButton = ({ product }: { product: ProductDetail }) => {
|
||||
return;
|
||||
}
|
||||
if (!product.seller?.id) return;
|
||||
createChatThread(product.seller.id, {
|
||||
onSuccess: (threadData) => {
|
||||
navigate(`/profile/threads/${threadData.id}`);
|
||||
},
|
||||
});
|
||||
createChatThread(
|
||||
{ participantId: product.seller.id, productId: product.id },
|
||||
{
|
||||
onSuccess: (threadData) => {
|
||||
navigate(`/profile/threads/${threadData.id}`);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -330,9 +330,12 @@ export const ProductDetailView = memo(function ProductDetailView({
|
||||
return;
|
||||
}
|
||||
if (!product.seller?.id) return;
|
||||
createChatThread(product.seller.id, {
|
||||
onSuccess: (t) => navigate(`/profile/threads/${t.id}`),
|
||||
});
|
||||
createChatThread(
|
||||
{ participantId: product.seller.id, productId: product.id },
|
||||
{
|
||||
onSuccess: (t) => navigate(`/profile/threads/${t.id}`),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -24,11 +24,14 @@ export const ProductHeader = ({ product }: ProductHeaderProps) => {
|
||||
return;
|
||||
}
|
||||
if (!product.seller?.id) return;
|
||||
createChatThread(product.seller.id, {
|
||||
onSuccess: (threadData) => {
|
||||
navigate(`/profile/threads/${threadData.id}`);
|
||||
},
|
||||
});
|
||||
createChatThread(
|
||||
{ participantId: product.seller.id, productId: product.id },
|
||||
{
|
||||
onSuccess: (threadData) => {
|
||||
navigate(`/profile/threads/${threadData.id}`);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Reply, MoreVertical, Trash2, Check } from "lucide-react";
|
||||
import { Link } from "@remix-run/react";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
@ -81,7 +82,34 @@ export const MessageBubble = ({
|
||||
{replyToMessageContent || "پاسخ به پیام"}
|
||||
</div>
|
||||
)}
|
||||
<p>{message.content}</p>
|
||||
{message.product && (
|
||||
<Link
|
||||
to={`/product/${message.product.id}`}
|
||||
className="flex items-center gap-2 mb-1 w-[210px] max-w-full rounded-lg bg-WHITE border border-GRAY3 p-2"
|
||||
>
|
||||
{message.product.primaryImage ? (
|
||||
<img
|
||||
src={message.product.primaryImage}
|
||||
alt=""
|
||||
className="w-12 h-12 rounded-md object-cover shrink-0"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded-md bg-WHITE3 shrink-0" />
|
||||
)}
|
||||
<div className="min-w-0 text-right">
|
||||
<p className="text-R12 text-BLACK truncate">
|
||||
{message.product.title}
|
||||
</p>
|
||||
{message.product.basePrice && (
|
||||
<p className="text-R10 text-GRAY mt-0.5">
|
||||
{parseInt(message.product.basePrice).toLocaleString("fa-IR")}{" "}
|
||||
تومان
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
{message.content && <p>{message.content}</p>}
|
||||
</div>
|
||||
<div className="flex flex-col justify-between items-start">
|
||||
<div className="flex flex-col gap-1 items-start">
|
||||
|
||||
@ -156,7 +156,10 @@ const ThreadsList = ({
|
||||
<p
|
||||
className={`text-R14 ${hasUnread ? "text-BLACK font-bold" : "text-GRAY"}`}
|
||||
>
|
||||
{thread.lastMessage?.content || "بدون پیام"}
|
||||
{thread.lastMessage?.content ||
|
||||
(thread.lastMessage?.product
|
||||
? `🛍 ${thread.lastMessage.product.title || "محصول"}`
|
||||
: "بدون پیام")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -206,15 +206,21 @@ export function useGetThreadMessages(threadId: string) {
|
||||
export function useCreateChatThread() {
|
||||
const token = useAuthToken();
|
||||
return useMutation({
|
||||
mutationFn: async (participantId: string) => {
|
||||
mutationFn: async (
|
||||
input: string | { participantId: string; productId?: string }
|
||||
) => {
|
||||
if (!token) {
|
||||
throw new Error("Authentication token and user ID are required");
|
||||
}
|
||||
// Back-compat: callers may pass a bare participantId string.
|
||||
const { participantId, productId } =
|
||||
typeof input === "string" ? { participantId: input, productId: undefined } : input;
|
||||
try {
|
||||
const chatApi = createChatApi(token);
|
||||
return await chatApi.apiChatV1ThreadsCreateCreate({
|
||||
data: {
|
||||
participantId: participantId,
|
||||
participantId,
|
||||
...(productId ? { productId } : {}),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@ -2336,14 +2336,51 @@ export interface LastMessage {
|
||||
*/
|
||||
readonly senderUsername?: string;
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof LastMessage
|
||||
*/
|
||||
readonly createdAt?: string;
|
||||
/**
|
||||
* Product shared into the chat, if this message is a product card.
|
||||
* @type {ChatProductCard}
|
||||
* @memberof LastMessage
|
||||
*/
|
||||
readonly product?: ChatProductCard | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* Minimal product info rendered as a card inside a chat.
|
||||
* @export
|
||||
* @interface ChatProductCard
|
||||
*/
|
||||
export interface ChatProductCard {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ChatProductCard
|
||||
*/
|
||||
readonly id?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ChatProductCard
|
||||
*/
|
||||
readonly title?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ChatProductCard
|
||||
*/
|
||||
readonly basePrice?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ChatProductCard
|
||||
*/
|
||||
readonly primaryImage?: string | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface Login
|
||||
*/
|
||||
@ -2479,11 +2516,17 @@ export interface MessageList {
|
||||
*/
|
||||
readonly isDeleted?: boolean;
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof MessageList
|
||||
*/
|
||||
readonly isThreadCreator?: boolean;
|
||||
/**
|
||||
* Product shared into the chat, if this message is a product card.
|
||||
* @type {ChatProductCard}
|
||||
* @memberof MessageList
|
||||
*/
|
||||
readonly product?: ChatProductCard | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@ -6464,6 +6507,12 @@ export interface ThreadCreate {
|
||||
* @memberof ThreadCreate
|
||||
*/
|
||||
participantId: string;
|
||||
/**
|
||||
* Optional product the chat is about; shared into the thread as a card
|
||||
* @type {string}
|
||||
* @memberof ThreadCreate
|
||||
*/
|
||||
productId?: string | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
|
||||
Loading…
Reference in New Issue
Block a user