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;
|
return;
|
||||||
}
|
}
|
||||||
if (!product.seller?.id) return;
|
if (!product.seller?.id) return;
|
||||||
createChatThread(product.seller.id, {
|
createChatThread(
|
||||||
onSuccess: (threadData) => {
|
{ participantId: product.seller.id, productId: product.id },
|
||||||
navigate(`/profile/threads/${threadData.id}`);
|
{
|
||||||
},
|
onSuccess: (threadData) => {
|
||||||
});
|
navigate(`/profile/threads/${threadData.id}`);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -330,9 +330,12 @@ export const ProductDetailView = memo(function ProductDetailView({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!product.seller?.id) return;
|
if (!product.seller?.id) return;
|
||||||
createChatThread(product.seller.id, {
|
createChatThread(
|
||||||
onSuccess: (t) => navigate(`/profile/threads/${t.id}`),
|
{ participantId: product.seller.id, productId: product.id },
|
||||||
});
|
{
|
||||||
|
onSuccess: (t) => navigate(`/profile/threads/${t.id}`),
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -24,11 +24,14 @@ export const ProductHeader = ({ product }: ProductHeaderProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!product.seller?.id) return;
|
if (!product.seller?.id) return;
|
||||||
createChatThread(product.seller.id, {
|
createChatThread(
|
||||||
onSuccess: (threadData) => {
|
{ participantId: product.seller.id, productId: product.id },
|
||||||
navigate(`/profile/threads/${threadData.id}`);
|
{
|
||||||
},
|
onSuccess: (threadData) => {
|
||||||
});
|
navigate(`/profile/threads/${threadData.id}`);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Reply, MoreVertical, Trash2, Check } from "lucide-react";
|
import { Reply, MoreVertical, Trash2, Check } from "lucide-react";
|
||||||
|
import { Link } from "@remix-run/react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -81,7 +82,34 @@ export const MessageBubble = ({
|
|||||||
{replyToMessageContent || "پاسخ به پیام"}
|
{replyToMessageContent || "پاسخ به پیام"}
|
||||||
</div>
|
</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>
|
||||||
<div className="flex flex-col justify-between items-start">
|
<div className="flex flex-col justify-between items-start">
|
||||||
<div className="flex flex-col gap-1 items-start">
|
<div className="flex flex-col gap-1 items-start">
|
||||||
|
|||||||
@ -156,7 +156,10 @@ const ThreadsList = ({
|
|||||||
<p
|
<p
|
||||||
className={`text-R14 ${hasUnread ? "text-BLACK font-bold" : "text-GRAY"}`}
|
className={`text-R14 ${hasUnread ? "text-BLACK font-bold" : "text-GRAY"}`}
|
||||||
>
|
>
|
||||||
{thread.lastMessage?.content || "بدون پیام"}
|
{thread.lastMessage?.content ||
|
||||||
|
(thread.lastMessage?.product
|
||||||
|
? `🛍 ${thread.lastMessage.product.title || "محصول"}`
|
||||||
|
: "بدون پیام")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -206,15 +206,21 @@ export function useGetThreadMessages(threadId: string) {
|
|||||||
export function useCreateChatThread() {
|
export function useCreateChatThread() {
|
||||||
const token = useAuthToken();
|
const token = useAuthToken();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (participantId: string) => {
|
mutationFn: async (
|
||||||
|
input: string | { participantId: string; productId?: string }
|
||||||
|
) => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error("Authentication token and user ID are required");
|
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 {
|
try {
|
||||||
const chatApi = createChatApi(token);
|
const chatApi = createChatApi(token);
|
||||||
return await chatApi.apiChatV1ThreadsCreateCreate({
|
return await chatApi.apiChatV1ThreadsCreateCreate({
|
||||||
data: {
|
data: {
|
||||||
participantId: participantId,
|
participantId,
|
||||||
|
...(productId ? { productId } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -2336,14 +2336,51 @@ export interface LastMessage {
|
|||||||
*/
|
*/
|
||||||
readonly senderUsername?: string;
|
readonly senderUsername?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof LastMessage
|
* @memberof LastMessage
|
||||||
*/
|
*/
|
||||||
readonly createdAt?: string;
|
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
|
* @export
|
||||||
* @interface Login
|
* @interface Login
|
||||||
*/
|
*/
|
||||||
@ -2479,11 +2516,17 @@ export interface MessageList {
|
|||||||
*/
|
*/
|
||||||
readonly isDeleted?: boolean;
|
readonly isDeleted?: boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @memberof MessageList
|
* @memberof MessageList
|
||||||
*/
|
*/
|
||||||
readonly isThreadCreator?: boolean;
|
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
|
* @memberof ThreadCreate
|
||||||
*/
|
*/
|
||||||
participantId: string;
|
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