import { useMutation, useQuery, useQueryClient, type QueryClient } from '@tanstack/react-query'; import type { ActiveChat, ConversationTriageStatus } from '../../../../../packages/api-contracts/src/index.js'; import { conversationApi } from './conversation-api.js'; import { ownedConversationId } from './conversation-identity.js'; export const conversationQueryKeys = { all: ['conversations'] as const, detail: (id: string) => ['conversations', 'detail', id] as const, processedPosts: (username: string) => ['conversations', 'processed-posts', username] as const, mediaShareOptions: (id: string) => ['conversations', 'media-share-options', id] as const, }; export const CONVERSATION_REFRESH_INTERVAL_MS = 30_000; export function useConversationsQuery() { return useQuery({ queryKey: conversationQueryKeys.all, queryFn: conversationApi.list, refetchInterval: CONVERSATION_REFRESH_INTERVAL_MS, }); } export function useConversationDetailQuery(id: string | null) { return useQuery({ queryKey: conversationQueryKeys.detail(id ?? ''), queryFn: () => conversationApi.detail(id ?? ''), enabled: id !== null, refetchInterval: CONVERSATION_REFRESH_INTERVAL_MS, }); } export function useSuggestReplyMutation(id: string) { return useMutation({ mutationFn: () => conversationApi.suggestReply(id) }); } export function useSuggestMediaAskMutation(id: string) { return useMutation({ mutationFn: () => conversationApi.suggestMediaAsk(id) }); } export function useConversationMediaShareOptionsQuery(id: string, enabled: boolean) { return useQuery({ queryKey: conversationQueryKeys.mediaShareOptions(id), queryFn: () => conversationApi.mediaShareOptions(id), enabled, }); } export function useProcessedPostsQuery(username: string) { return useQuery({ queryKey: conversationQueryKeys.processedPosts(username), queryFn: () => conversationApi.processedPosts(username), enabled: username.length > 0, }); } export function useSendReplyMutation(id: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: (message: string) => conversationApi.sendReply(id, message), async onSuccess() { await Promise.all([ queryClient.invalidateQueries({ queryKey: conversationQueryKeys.all }), queryClient.invalidateQueries({ queryKey: conversationQueryKeys.detail(id) }), ]); }, }); } export function useShareConversationMediaMutation(id: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: (assetId: string) => conversationApi.shareMedia(id, assetId), async onSuccess() { await Promise.all([ queryClient.invalidateQueries({ queryKey: conversationQueryKeys.all }), queryClient.invalidateQueries({ queryKey: conversationQueryKeys.detail(id) }), queryClient.invalidateQueries({ queryKey: conversationQueryKeys.mediaShareOptions(id) }), ]); }, }); } /** * Applies a triage patch to the cached list and detail entries for one * conversation. Read-state writes are optimistic because the list only polls * every 30s — waiting for a refetch would make marking read feel broken. */ function patchCachedConversation( queryClient: QueryClient, id: string, patch: Partial, ): void { queryClient.setQueryData(conversationQueryKeys.all, (current) => current?.map((conversation) => (ownedConversationId(conversation) === id ? { ...conversation, ...patch } : conversation))); queryClient.setQueryData( conversationQueryKeys.detail(id), (current) => (current ? { ...current, ...patch } : current), ); } function cachedConversation(queryClient: QueryClient, id: string): ActiveChat | undefined { return queryClient.getQueryData(conversationQueryKeys.all) ?.find((conversation) => ownedConversationId(conversation) === id); } export function useMarkConversationReadMutation() { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ id, throughTimestamp }: { id: string; throughTimestamp?: number }) => conversationApi.markRead(id, throughTimestamp), onMutate({ id, throughTimestamp }) { const watermark = throughTimestamp ?? cachedConversation(queryClient, id)?.lastMessageTime ?? 0; patchCachedConversation(queryClient, id, { hasUnreadInbound: false, lastReadMessageTs: watermark }); }, async onSettled() { await queryClient.invalidateQueries({ queryKey: conversationQueryKeys.all }); }, }); } export function useMarkConversationUnreadMutation() { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ id, beforeTimestamp }: { id: string; beforeTimestamp?: number }) => conversationApi.markUnread(id, beforeTimestamp), onMutate({ id, beforeTimestamp }) { const boundary = beforeTimestamp ?? cachedConversation(queryClient, id)?.lastMessageTime ?? 0; patchCachedConversation(queryClient, id, { hasUnreadInbound: true, triageStatus: 'open', snoozedUntil: null, lastReadMessageTs: Math.max(0, boundary - 1), }); }, async onSettled() { await queryClient.invalidateQueries({ queryKey: conversationQueryKeys.all }); }, }); } export function useSetConversationStatusMutation() { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ id, status, snoozeUntil }: { id: string; status: ConversationTriageStatus; snoozeUntil?: number | null; }) => conversationApi.setStatus(id, status, snoozeUntil), onMutate({ id, status, snoozeUntil }) { const conversation = cachedConversation(queryClient, id); patchCachedConversation(queryClient, id, { triageStatus: status, snoozedUntil: status === 'snoozed' ? snoozeUntil ?? null : null, // Resolving a conversation implies you have read it. ...(status === 'done' ? { hasUnreadInbound: false, lastReadMessageTs: conversation?.lastMessageTime ?? 0 } : {}), needsReply: status === 'open' && conversation?.isFromUs === false, }); }, async onSettled() { await queryClient.invalidateQueries({ queryKey: conversationQueryKeys.all }); }, }); } export function useBulkMarkConversationsReadMutation() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (ids: string[]) => conversationApi.bulkMarkRead(ids), onMutate(ids) { for (const id of ids) { const watermark = cachedConversation(queryClient, id)?.lastMessageTime ?? 0; patchCachedConversation(queryClient, id, { hasUnreadInbound: false, lastReadMessageTs: watermark }); } }, async onSettled() { await queryClient.invalidateQueries({ queryKey: conversationQueryKeys.all }); }, }); } /** * Forces a seller pass over every active conversation. The list is refetched * on completion because a sweep can flag, kill and resolve conversations. */ export function useSellerScanMutation() { const queryClient = useQueryClient(); return useMutation({ mutationFn: () => conversationApi.sellerScan(), async onSuccess() { await queryClient.invalidateQueries({ queryKey: conversationQueryKeys.all }); }, }); } export function useStartConversationMutation() { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ accountId, username, message }: { accountId: string; username: string; message: string }) => conversationApi.startConversation(accountId, username, message), async onSuccess() { await queryClient.invalidateQueries({ queryKey: conversationQueryKeys.all }); }, }); }