Files
baya-monorepo/client/src/components/messaging/TicketConversationPanel.tsx
T
2026-07-19 17:13:32 +03:30

81 lines
2.9 KiB
TypeScript

'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<TicketConversationPanelProps> = ({ 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 (
<>
<TicketMessageList ticketId={ticketId} onRetry={retry} onDiscard={discard} />
<Box
sx={{
position: 'sticky',
bottom: 0,
bgcolor: 'var(--bal-bg-default)',
borderTop: '1px solid',
borderColor: 'divider',
pt: 1,
pb: 0.5,
}}
>
{closed ? (
<Typography variant="body2" sx={{ textAlign: 'center', color: 'var(--bal-text-secondary)', py: 1 }}>
{t('closed_notice')}
</Typography>
) : (
<MessageComposer value={draft} onChange={setDraft} onSubmit={submit} sending={sending} />
)}
</Box>
</>
);
};
export default TicketConversationPanel;