Vitron-Front/app/requestHandler/use-chat-hooks.tsx
Arda Samadi 7867db425e 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>
2026-07-13 15:09:01 +03:30

233 lines
5.9 KiB
TypeScript

import {
useQuery,
useInfiniteQuery,
QueryClient,
useMutation,
} from "@tanstack/react-query";
import { useAuthToken } from "../hooks/use-root-data";
import { MessageList } from "../../src/api/types/models";
import { createChatApi } from "~/utils/api-client-factory";
/**
* Custom hook to get the list of user's threads
*/
export function useGetUserThreads() {
const token = useAuthToken();
return useQuery({
queryKey: ["getUserThreads"],
queryFn: async () => {
if (!token) {
throw new Error("Authentication token is required");
}
const chatApi = createChatApi(token);
return await chatApi.apiChatV1ThreadsList();
},
enabled: !!token,
retry: 1,
retryDelay: 3000,
refetchOnWindowFocus: false,
refetchInterval: 20 * 1000, // 30 seconds refetch
});
}
/**
* List a STORE's inbox (owner + staff), scoped to the store's own threads.
* Distinct from useGetUserThreads(), which returns the user's personal buyer chats.
*/
export function useGetStoreThreads(storeId?: string) {
const token = useAuthToken();
return useQuery({
queryKey: ["getStoreThreads", storeId],
queryFn: async () => {
if (!token) {
throw new Error("Authentication token is required");
}
const chatApi = createChatApi(token);
return await chatApi.apiChatV1ThreadsListList({ storeId });
},
enabled: !!token && !!storeId,
retry: 1,
retryDelay: 3000,
refetchOnWindowFocus: false,
refetchInterval: 20 * 1000,
});
}
/**
* Server-side function to get thread details
*/
export async function getThreadDetails(token: string, threadId: string) {
if (!token) {
throw new Error("Authentication token is required");
}
const chatApi = createChatApi(token);
return await chatApi.apiChatV1ThreadsRead({ threadId });
}
/**
* Custom hook to get thread details by ID
*/
export function useGetThreadDetails(threadId: string) {
const token = useAuthToken();
return useQuery({
queryKey: ["getThreadDetails", threadId],
queryFn: async () => {
if (!token) {
throw new Error("Authentication token is required");
}
const chatApi = createChatApi(token);
return await chatApi.apiChatV1ThreadsRead({ threadId });
},
enabled: !!token && !!threadId,
retry: 1,
retryDelay: 3000,
refetchOnWindowFocus: false,
});
}
export const deleteMessageFromCache = (
queryClient: QueryClient,
threadId: string,
messageId: string
) => {
queryClient.setQueryData(
["getThreadMessages", threadId],
(
oldData:
| {
pages: Array<{
results: MessageList[];
nextCursor: string | null;
}>;
pageParams: Array<string | undefined>;
}
| undefined
) => {
if (!oldData) return oldData;
return {
...oldData,
pages: oldData.pages.map((page) => {
return {
...page,
results: page.results.filter((message) => message.id !== messageId),
};
}),
};
}
);
};
export const setMessagesCache = (
queryClient: QueryClient,
threadId: string,
data: MessageList
) => {
queryClient.setQueryData(
["getThreadMessages", threadId],
(
oldData:
| {
pages: Array<{
results: MessageList[];
nextCursor: string | null;
}>;
pageParams: Array<string | undefined>;
}
| undefined
) => {
if (!oldData) return oldData;
return {
...oldData,
pages: oldData.pages.map((page, index: number) => {
if (index === 0) {
return {
...page,
results: [data, ...page.results],
};
}
return page;
}),
};
}
);
};
/**
* Custom hook to get thread messages with infinite scrolling
*/
export function useGetThreadMessages(threadId: string) {
const token = useAuthToken();
return useInfiniteQuery({
queryKey: ["getThreadMessages", threadId],
queryFn: async ({ pageParam }) => {
if (!token) {
throw new Error("Authentication token is required");
}
const chatApi = createChatApi(token);
// Using cursor-based pagination
const response = await chatApi.apiChatV1ThreadsMessagesList({
threadId,
cursor: pageParam ? String(pageParam) : undefined,
});
// Check if response has next cursor
let nextCursor;
if (response.next) {
const nextUrlParts = response.next.split("?cursor=");
nextCursor = nextUrlParts.length > 1 ? nextUrlParts[1] : null;
} else {
nextCursor = null;
}
const messages = response.results || [];
// Get the ID of the last message to use as next cursor
return {
results: messages,
nextCursor,
};
},
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => {
return lastPage.nextCursor || undefined;
},
enabled: !!token && !!threadId,
refetchOnWindowFocus: false,
});
}
/**
* Custom hook to create a new chat thread
*/
export function useCreateChatThread() {
const token = useAuthToken();
return useMutation({
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,
...(productId ? { productId } : {}),
},
});
} catch (error) {
console.error("Error creating chat thread:", error);
throw error;
}
},
});
}