import type { ConversationParticipantResponseDto, ConversationResponseDto, MessageReplyToDto, MessageResponseDto, SendMessageDto, } from '@/api/generated/models' import { createServiceError, errorResult, handleServiceError, type ServiceCallOptions, type ServiceResult, shouldBubbleErrorToParent, successResult, } from '@/services/errorHandler' import { unwrapApiData } from '@/services/apiResponse' import { getChat } from '@/api/generated/chat/chat' import { texts } from '@/texts' import { parseRemittanceList } from '@/helpers/listResponse' export type Conversation = ConversationResponseDto export type ChatMessage = MessageResponseDto export type ChatMessageReplyTo = MessageReplyToDto export type ConversationTypeFilter = 'direct' | 'event_group' export const CONVERSATIONS_PAGE_SIZE = 20 /** Direction-based history page (same field meaning in before / after / around modes). */ export interface ChatMessagesPage { items: ChatMessage[] olderCursor: string | null newerCursor: string | null hasMoreBefore: boolean hasMoreAfter: boolean aroundMessageId: string | null } export interface ListMessagesParams { before?: string after?: string around?: string pageSize?: number } export type SendChatMessagePayload = Pick export interface ConversationsPage { items: Conversation[] totalItemsCount: number } const chatApi = getChat() export const LIST_CONVERSATIONS = async ( params?: { type?: ConversationTypeFilter; page?: number; pageSize?: number }, options?: ServiceCallOptions ): Promise> => { const page = params?.page ?? 1 const pageSize = params?.pageSize ?? CONVERSATIONS_PAGE_SIZE try { const res = await chatApi.chatControllerListMine({ page, pageSize, sort: '-lastMessageAt', ...(params?.type ? { 'filters[type]': params.type } : {}), }) const payload = res.data if (!payload.success) { throw createServiceError(payload.message || texts.chats.listConversationsFailed) } const parsed = parseRemittanceList(unwrapApiData(payload) ?? {}, 'items', page, pageSize) return successResult({ items: parsed.items, totalItemsCount: parsed.pagination.totalItemsCount }) } catch (error) { const normalizedError = handleServiceError(error, options) if (shouldBubbleErrorToParent(options)) { throw normalizedError } return errorResult(normalizedError) } } export const GET_CONVERSATION = async (conversationId: string, options?: ServiceCallOptions): Promise> => { try { const res = await chatApi.chatControllerGetOne(conversationId) const payload = res.data if (!payload.success) { throw createServiceError(payload.message || texts.chats.getConversationFailed) } const conversation = unwrapApiData(payload) if (!conversation?.id) { throw createServiceError(texts.common.invalidServerResponse) } return successResult(conversation) } catch (error) { const normalizedError = handleServiceError(error, options) if (shouldBubbleErrorToParent(options)) { throw normalizedError } return errorResult(normalizedError) } } export const GET_UNREAD_CHAT_COUNT = async (options?: ServiceCallOptions): Promise> => { try { const res = await chatApi.chatControllerUnreadCount() const payload = res.data if (!payload.success) { throw createServiceError(payload.message || texts.chats.unreadCountFailed) } const data = unwrapApiData(payload) return successResult({ totalUnread: data?.totalUnread ?? 0 }) } catch (error) { const normalizedError = handleServiceError(error, options) if (shouldBubbleErrorToParent(options)) { throw normalizedError } return errorResult(normalizedError) } } export const MARK_CONVERSATION_READ = async ( conversationId: string, lastSeenMessageId: string, options?: ServiceCallOptions ): Promise> => { try { const res = await chatApi.chatControllerMarkRead(conversationId, { lastSeenMessageId }) const payload = res.data if (!payload.success) { throw createServiceError(payload.message || texts.chats.markReadFailed) } const data = unwrapApiData(payload) return successResult({ totalUnread: data?.totalUnread ?? 0 }) } catch (error) { const normalizedError = handleServiceError(error, options) if (shouldBubbleErrorToParent(options)) { throw normalizedError } return errorResult(normalizedError) } } const MESSAGES_PAGE_SIZE = 50 /** * Fetches one page of conversation history. * * Cursor modes are mutually exclusive (`before` | `after` | `around`). Response * cursors are direction-based: `olderCursor` → next `before`, `newerCursor` → * next `after`, regardless of which mode produced the page. */ export const LIST_MESSAGES = async ( conversationId: string, params?: ListMessagesParams, options?: ServiceCallOptions ): Promise> => { try { const pageSize = params?.pageSize ?? MESSAGES_PAGE_SIZE const res = await chatApi.chatControllerListMessages(conversationId, { pageSize, ...(params?.before ? { before: params.before } : {}), ...(params?.after ? { after: params.after } : {}), ...(params?.around ? { around: params.around } : {}), }) const payload = res.data if (!payload.success) { throw createServiceError(payload.message || texts.chats.listMessagesFailed) } const data = unwrapApiData(payload) const parsed = parseRemittanceList(data ?? {}, 'items', 1, pageSize) const meta = data?.response return successResult({ items: parsed.items, olderCursor: meta?.olderCursor ?? null, newerCursor: meta?.newerCursor ?? null, hasMoreBefore: meta?.hasMoreBefore ?? false, hasMoreAfter: meta?.hasMoreAfter ?? false, aroundMessageId: meta?.aroundMessageId ?? null, }) } catch (error) { const normalizedError = handleServiceError(error, options) if (shouldBubbleErrorToParent(options)) { throw normalizedError } return errorResult(normalizedError) } } export const SEND_MESSAGE = async ( conversationId: string, payload: SendChatMessagePayload, options?: ServiceCallOptions ): Promise> => { try { const res = await chatApi.chatControllerSendGroup(conversationId, payload) const responsePayload = res.data if (!responsePayload.success) { throw createServiceError(responsePayload.message || texts.chats.sendFailed, responsePayload.code) } const message = unwrapApiData(responsePayload) if (!message?.id) { throw createServiceError(texts.common.invalidServerResponse) } return successResult(message) } catch (error) { const normalizedError = handleServiceError(error, options) if (shouldBubbleErrorToParent(options)) { throw normalizedError } return errorResult(normalizedError) } } /** * Sends the first (or a subsequent) message of a direct conversation by the * other participant's user id, without needing a conversationId up front. * Backend resolves/creates the canonical direct conversation for the pair * (`get_or_create_direct_conversation`) and inserts the message in one call — * see docs/workflows/fa/chat.md, "جریان A — پیام مستقیم". */ export const SEND_DIRECT_MESSAGE = async ( otherUserId: string, payload: SendChatMessagePayload, options?: ServiceCallOptions ): Promise> => { try { const res = await chatApi.chatControllerSendDirect(otherUserId, payload) const responsePayload = res.data if (!responsePayload.success) { throw createServiceError(responsePayload.message || texts.chats.sendFailed, responsePayload.code) } const message = unwrapApiData(responsePayload) if (!message?.id) { throw createServiceError(texts.common.invalidServerResponse) } return successResult(message) } catch (error) { const normalizedError = handleServiceError(error, options) if (shouldBubbleErrorToParent(options)) { throw normalizedError } return errorResult(normalizedError) } } export const ENSURE_DIRECT_CONVERSATION = async ( otherUserId: string, options?: ServiceCallOptions ): Promise> => { try { const res = await chatApi.chatControllerEnsureDirect(otherUserId) const payload = res.data if (!payload.success) { throw createServiceError(payload.message || texts.chats.openConversationFailed, payload.code) } const conversation = unwrapApiData(payload) if (!conversation?.id) { throw createServiceError(texts.common.invalidServerResponse) } return successResult(conversation) } catch (error) { const normalizedError = handleServiceError(error, options) if (shouldBubbleErrorToParent(options)) { throw normalizedError } return errorResult(normalizedError) } } export type ChatParticipant = ConversationParticipantResponseDto export const LIST_PARTICIPANTS = async ( conversationId: string, params?: { page?: number; pageSize?: number }, options?: ServiceCallOptions ): Promise> => { try { const res = await chatApi.chatControllerListParticipants(conversationId, { page: params?.page ?? 1, pageSize: params?.pageSize ?? 100, sort: 'joinedAt', }) const payload = res.data if (!payload.success) { throw createServiceError(payload.message || texts.chats.listParticipantsFailed) } const parsed = parseRemittanceList(unwrapApiData(payload) ?? {}, 'items') return successResult({ items: parsed.items }) } catch (error) { const normalizedError = handleServiceError(error, options) if (shouldBubbleErrorToParent(options)) { throw normalizedError } return errorResult(normalizedError) } }