# ChatMessage

**When to use:** One message in a conversation: a messenger bubble, a support agent's reply, an AI assistant's answer. Put them in a [ChatMessageList](ChatMessageList.md), which also shows a whole chat put together. For a note between messages ("Lisa joined", a date), use a [Divider](Divider.md) with `text`.

**Keywords:** speech bubble, messenger, whatsapp, chatbot, assistant, LLM, reply, read receipt, seen, ticks, typing indicator

**Import:** `import { ChatMessage, ChatMessageStyleType, ChatMessageStatus } from '@ahrowe/ui'`  
**Types:** `import type { ChatMessageProps, ChatMessageLabels } from '@ahrowe/ui'`

```tsx
import { Avatar, ChatMessage, ChatMessageStatus, ChatMessageStyleType, CopyButton, Loader, LoaderType, ThemeColor } from '@ahrowe/ui';

// Messenger: two from Ava in a run, then your own with its delivery state
<ChatMessage author='Ava' avatar={<Avatar name='Ava' />} time='10:41'>Are we still on for tonight?</ChatMessage>
<ChatMessage author='Ava' avatar={<Avatar name='Ava' />} time='10:41' isContinued>8 o'clock?</ChatMessage>
<ChatMessage isOwn time='10:42' status={ChatMessageStatus.Read}>Yes, see you there</ChatMessage>

// AI assistant: no bubble, full width, actions underneath
<ChatMessage styleType={ChatMessageStyleType.Plain} actions={<CopyButton value={answer} />}>
  <Markdown>{answer}</Markdown>
</ChatMessage>

// Someone typing, or an answer on its way: a Loader is the content
<ChatMessage avatar={<Avatar name='Ava' />}>
  <Loader type={LoaderType.Dots} size='1em' color={ThemeColor.Neutral} labels={{ loading: 'Ava is typing' }} />
</ChatMessage>
```

**Enums:**

- `ChatMessageStyleType`: `Filled` (default when `isOwn`) | `Light` (default otherwise) | `Plain`, see [ThemeColor.md](ThemeColor.md). A `Light` bubble keeps the page's text colour, as a `Light` Card does. `Plain` has no bubble and takes the full width, for an assistant's answer
- `ChatMessageStatus`: `Sending` (a clock) | `Sent` (a check) | `Delivered` (two checks) | `Read` (two checks at full strength, in `--info-color` except on a `Filled` bubble, where the rest are dimmed) | `Failed` (no icon in the bubble: "Not sent" under it, ahead of `actions`, where a retry button belongs, with the icon in `--error-color` and the text in the page's own)

**A run of messages.** Set `isContinued` on every message after the first from the same author:
it hides `author` from view, tightens the gap above, rounds off the corner that points at the
author, and keeps the space of `avatar` so the run stays aligned. Pass the same `author` and
`avatar` to all of them: screen readers still read `author` on each one. Start a new run after a
date `Divider`, or the first message under it shows no name.

**Who sent it, for screen readers.** Alignment and colour are not read out, so each message names
its sender: `author`, or "You" (`labels.you`, never shown) on an own message without one. In a
one-to-one chat that is enough to leave `author` out; in a group, pass it to everyone else's.

```tsx
{messages.map((message, index) => (
  <ChatMessage
    key={message.id}
    isOwn={message.authorId === me.id}
    isContinued={messages[index - 1]?.authorId === message.authorId}
    author={message.authorId === me.id ? undefined : message.authorName}
    avatar={message.authorId === me.id ? undefined : <Avatar name={message.authorName} src={message.avatarUrl} />}
    time={timeFormat.format(message.sentAt)}
    status={message.status}
  >
    {message.text}
  </ChatMessage>
))}
```

**Content.** A string keeps its line breaks, so text typed with Shift+Enter shows as typed. Any
other content renders as given: the library renders no Markdown, so pass your renderer's output
(`react-markdown`, `marked`). A long word or URL wraps rather than widening the bubble. `time` and
`status` sit at the bubble's end: on the same line as a short message, on a line of their own
under a long one.

**Key props:**

| Prop | Type | Description |
|------|------|-------------|
| `children` | `ReactNode` | The message |
| `isOwn` | `boolean` | Sent by the current user: aligned to the end, `Filled` in `Primary` by default |
| `isContinued` | `boolean` | Follows a message from the same author, see above |
| `avatar` | `ReactNode` | Beside the bubble, typically an [Avatar](Avatar.md) |
| `author` | `ReactNode` | Name above the bubble, on the first message of a run. Screen readers get it on every one |
| `time` | `ReactNode` | In the bubble's corner. Format it yourself, e.g. with `Intl.DateTimeFormat` |
| `status` | `ChatMessageStatus` | Delivery state, an icon after `time` that screen readers read out. `Failed` is a notice under the bubble instead |
| `actions` | `ReactNode` | Under the bubble, after the `Failed` notice: a [CopyButton](CopyButton.md), retry, thumbs up and down |
| `styleType` | `ChatMessageStyleType` | See above |
| `color` | `ColorValue` | A `ThemeColor` or any CSS colour. Default `Primary` when `isOwn`, otherwise `Neutral` |

The bubble is at most 80% of the list's width (40em at most); `styles={{ body: { maxWidth } }}`
changes that.

**Global defaults:** adopts `ConfigProvider`. See [ConfigProvider.md](ConfigProvider.md).

**Slots:** `avatar` `body` (the column holding author, bubble and actions) `author` `bubble` `meta` (time and status) `actions` (the row under the bubble, with the `Failed` notice)

**Labels:** `sending` `sent` `delivered` `read` (the names screen readers give the status icon), `failed` (the notice under a failed message) and `you` (an own message's sender, for screen readers only). Pass `labels` to override any of them, on the component or app-wide through `ConfigProvider`; they merge per key. Type: `ChatMessageLabels`.
