From 7867db425e124b887169ce2782ef9e316b8cab1d Mon Sep 17 00:00:00 2001 From: Arda Samadi Date: Mon, 13 Jul 2026 15:09:01 +0330 Subject: [PATCH] feat(chat): share the product into the chat + fix empty-thread preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 🛍 instead of "بدون پیام" for product-card threads. ChatProductCard type + product on message/last-message types. Co-Authored-By: Claude Opus 4.8 --- app/components/product/ContactButton.tsx | 13 +++-- app/components/product/ProductDetailView.tsx | 9 ++-- app/components/product/ProductHeader.tsx | 13 +++-- app/components/thread/MessageBubble.tsx | 30 ++++++++++- app/components/thread/ThreadsPage.tsx | 5 +- app/requestHandler/use-chat-hooks.tsx | 10 +++- src/api/types/models/index.ts | 55 ++++++++++++++++++-- 7 files changed, 115 insertions(+), 20 deletions(-) diff --git a/app/components/product/ContactButton.tsx b/app/components/product/ContactButton.tsx index 4099cb9..a2478ac 100644 --- a/app/components/product/ContactButton.tsx +++ b/app/components/product/ContactButton.tsx @@ -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 ( <> diff --git a/app/components/product/ProductDetailView.tsx b/app/components/product/ProductDetailView.tsx index 6a28cc5..e0db283 100644 --- a/app/components/product/ProductDetailView.tsx +++ b/app/components/product/ProductDetailView.tsx @@ -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 ( diff --git a/app/components/product/ProductHeader.tsx b/app/components/product/ProductHeader.tsx index 63313af..9ec1956 100644 --- a/app/components/product/ProductHeader.tsx +++ b/app/components/product/ProductHeader.tsx @@ -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 ( diff --git a/app/components/thread/MessageBubble.tsx b/app/components/thread/MessageBubble.tsx index 8d17670..2f12749 100644 --- a/app/components/thread/MessageBubble.tsx +++ b/app/components/thread/MessageBubble.tsx @@ -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 || "پاسخ به پیام"} )} -

{message.content}

+ {message.product && ( + + {message.product.primaryImage ? ( + + ) : ( +
+ )} +
+

+ {message.product.title} +

+ {message.product.basePrice && ( +

+ {parseInt(message.product.basePrice).toLocaleString("fa-IR")}{" "} + تومان +

+ )} +
+ + )} + {message.content &&

{message.content}

}
diff --git a/app/components/thread/ThreadsPage.tsx b/app/components/thread/ThreadsPage.tsx index eb949c3..ff310bb 100644 --- a/app/components/thread/ThreadsPage.tsx +++ b/app/components/thread/ThreadsPage.tsx @@ -156,7 +156,10 @@ const ThreadsList = ({

- {thread.lastMessage?.content || "بدون پیام"} + {thread.lastMessage?.content || + (thread.lastMessage?.product + ? `🛍 ${thread.lastMessage.product.title || "محصول"}` + : "بدون پیام")}

diff --git a/app/requestHandler/use-chat-hooks.tsx b/app/requestHandler/use-chat-hooks.tsx index a92b739..4c01ae5 100644 --- a/app/requestHandler/use-chat-hooks.tsx +++ b/app/requestHandler/use-chat-hooks.tsx @@ -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) { diff --git a/src/api/types/models/index.ts b/src/api/types/models/index.ts index 6487d61..9835651 100644 --- a/src/api/types/models/index.ts +++ b/src/api/types/models/index.ts @@ -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; } /** *