'use client'; import { FunctionComponent, useState } from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import { useTranslations } from 'next-intl'; import { useDiscardFailedMessage, usePostMessage } from '@/services/tickets'; import type { TicketMessage } from '@/services/tickets/types'; import { makeClientMessageId } from './clientMessageId'; import MessageComposer from './MessageComposer'; import TicketMessageList from './TicketMessageList'; export interface TicketConversationPanelProps { ticketId: number; /** Closed tickets show a notice instead of the composer. */ closed: boolean; } /** * The interactive body of a thread — the message list + the sticky composer, owning the one * `usePostMessage` mutation + draft state both share (so a failed bubble's retry/discard and the * composer's own send are the same optimistic pipeline, never two disconnected ones). Mounted **keyed by * `ticketId`** from `TicketThreadScreen` so navigating thread→thread (the App Router reuses the `[id]` * subtree) remounts this whole panel — the draft/in-flight/failed state never crosses tickets (§5). * @component TicketConversationPanel */ const TicketConversationPanel: FunctionComponent = ({ ticketId, closed }) => { const t = useTranslations('tickets'); const [draft, setDraft] = useState(''); const postMessage = usePostMessage(ticketId); const discardFailedMessage = useDiscardFailedMessage(); const sending = postMessage.isPending; const submit = () => { const body = draft.trim(); if (!body || sending || closed) return; postMessage.mutate( { body, clientMessageId: makeClientMessageId() }, { onSuccess: () => setDraft('') }, // clear the draft ONLY on server confirm (§3.5) ); }; const retry = (message: TicketMessage) => { if (!message.clientMessageId) return; postMessage.mutate({ body: message.body, clientMessageId: message.clientMessageId }); }; const discard = (message: TicketMessage) => { if (!message.clientMessageId) return; discardFailedMessage(ticketId, message.clientMessageId); setDraft(message.body); // a failure never loses typed text — restore it for editing (§3.3) }; return ( <> {closed ? ( {t('closed_notice')} ) : ( )} ); }; export default TicketConversationPanel;